Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog

For releases before 0.3.6, see the [GitHub releases](https://github.com/hawkff/pi-cliproxyapi-native/releases).

## [0.3.6] - 2026-11-10

- Pi's background prompt-cache refreshes skip the quota wait. Before, a refresh that hit a quota limit showed a quota countdown while Pi sat idle, then sent the whole request again after the reset.
- Claude on unprefixed, `oauth/`, `vertex/`, and `antigravity/` routes reports a one-hour cache lifetime, so Pi stops refreshing those caches every five minutes. CLIProxyAPI keeps OAuth Claude caches for an hour. `api/` routes keep Pi's five-minute refreshes.
- Claude message threads stay off. A proxy route that drops the `thread` field would send only the newest messages.
- Live Claude quota lookups send the Claude Code version that CLIProxyAPI 8.0.63 uses.
- The `/cli:model` picker reports Pi as waiting for input in terminals that show program status.
- The extension targets Pi 1.5.1 and CLIProxyAPI 8.0.63.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pi-cliproxyapi-native",
"version": "0.3.5",
"version": "0.3.6",
"description": "CLIProxyAPI chat discovery, image generation and editing, and video generation with Pi's native adapters",
"publishConfig": {
"access": "public"
Expand All @@ -23,6 +23,7 @@
"extensions",
"src",
"README.md",
"CHANGELOG.md",
"LICENSE"
],
"pi": {
Expand All @@ -39,9 +40,9 @@
},
"devDependencies": {
"@biomejs/biome": "^2.6.5",
"@earendil-works/pi-ai": "^1.5.0",
"@earendil-works/pi-coding-agent": "^1.5.0",
"@earendil-works/pi-tui": "^1.5.0",
"@earendil-works/pi-ai": "^1.5.1",
"@earendil-works/pi-coding-agent": "^1.5.1",
"@earendil-works/pi-tui": "^1.5.1",
"@types/node": "^24.19.2",
"typebox": "1.3.27",
"typescript": "7.0.2"
Expand Down
92 changes: 46 additions & 46 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ export function mapCatalog(value: unknown, config: Config, known: readonly Model
api,
baseUrl: endpoint(config.baseUrl, api),
headers: undefined,
// CLIProxyAPI gives OAuth Claude caches a one-hour TTL. Only api/ routes keep Pi's five-minute refreshes.
...(api === "anthropic-messages" && prefix !== "api"
? { promptCache: { short: 3600, long: 3600 } }
: {}),
compat:
api === "anthropic-messages"
? {
Expand All @@ -374,6 +378,8 @@ export function mapCatalog(value: unknown, config: Config, known: readonly Model
supportsMidConvoSystemMessages: false,
supportsMidConvoToolChanges: false,
supportsMidConvoEffort: false,
// A route that drops the thread field would send only the newest messages.
supportsMessageThreads: false,
allowedFallbackModels: undefined,
}
: api === "google-generative-ai"
Expand Down
3 changes: 2 additions & 1 deletion src/claude-quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ export async function fetchClaudeQuota(
Authorization: "Bearer $TOKEN$",
"Content-Type": "application/json",
"anthropic-beta": "oauth-2025-04-20",
"User-Agent": "claude-cli/2.1.280 (external, cli)",
// CLIProxyAPI 8.0.63 presents this Claude Code version for OAuth accounts.
"User-Agent": "claude-cli/2.1.321 (external, cli)",
},
});
if (!isRecord(response) || typeof response.status_code !== "number")
Expand Down
1 change: 1 addition & 0 deletions src/picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export function registerModelPicker(pi: ExtensionAPI, config: Config) {
() => tui.requestRender(),
done,
),
{ blocked: { kind: "question", message: "Choose a CLIProxyAPI image or video model" } },
);
} else {
const matches = searchPickerItems(items, query);
Expand Down
7 changes: 5 additions & 2 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ function route(
options?.env?.CLIPROXYAPI_MANAGEMENT_KEY ?? process.env.CLIPROXYAPI_MANAGEMENT_KEY ?? "";
const liveClaude =
model.api === "anthropic-messages" && !backend && authType !== "api-key" && managementKey !== "";
// Pi's cache warmer replays a request with these options. A delayed replay cannot keep a cache warm.
const warming =
options?.disableContinuation === true && options.maxTokens === 1 && options.maxRetries === 0;
const adapterModel = google ? { ...model, id: metadataId } : model;
// Google needs canonical IDs for tool turns. Swap identities to keep bare history cross-route.
const adapterContext = google
Expand Down Expand Up @@ -231,7 +234,7 @@ function route(
let started = false;
let retry = false;
for await (const event of run()) {
if (event.type === "error" && event.reason === "error" && !started) {
if (event.type === "error" && event.reason === "error" && !started && !warming) {
const resetAt = quotaRetryAt(event.error.errorMessage, response);
const failure = quotaError(event.error.errorMessage);
const upstream =
Expand Down Expand Up @@ -320,7 +323,7 @@ export function createCliproxyProvider(
let skipped: string[] = [];
const standardAuth = envApiKeyAuth("CLIProxyAPI API key", ["CLIPROXYAPI_API_KEY"]);
const scope = createHash("sha256")
.update(JSON.stringify([5, { baseUrl: config.baseUrl, aliases: config.aliases }]))
.update(JSON.stringify([6, { baseUrl: config.baseUrl, aliases: config.aliases }]))
.digest("hex");
const provider = createProvider<CpaApi>({
id: PROVIDER_ID,
Expand Down
7 changes: 6 additions & 1 deletion test/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ test("npm package contains only release files and loads without checkout depende
})
.sort(),
[
"CHANGELOG.md",
"LICENSE",
"README.md",
"extensions/index.ts",
Expand All @@ -54,7 +55,11 @@ test("npm package contains only release files and loads without checkout depende
timeout: 10000,
});
const manifest: unknown = JSON.parse(await readFile(join(directory, "package/package.json"), "utf8"));
assert.ok(isRecord(manifest) && Array.isArray(manifest.keywords));
assert.ok(isRecord(manifest) && Array.isArray(manifest.keywords) && typeof manifest.version === "string");
// After an update, Pi shows the changelog entries newer than the previously installed version.
const heading = `## [${manifest.version}] - `;
const changelog = await readFile(join(directory, "package/CHANGELOG.md"), "utf8");
assert.ok(changelog.split("\n").some((line) => line.startsWith(heading)));
for (const keyword of ["pi-package", "pi-extension", "cliproxyapi"])
assert.ok(manifest.keywords.includes(keyword));
assert.ok(isRecord(manifest.peerDependencies));
Expand Down
16 changes: 12 additions & 4 deletions test/picker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,7 @@ test("media picker and listings show derived defaults without footer output or u
const sessionManager = SessionManager.inMemory();
let hooks = 0;
let footerWrites = 0;
let opened: { rendered: string; options: Parameters<ExtensionContext["ui"]["custom"]>[1] } | undefined;
const output: string[] = [];
let command: Parameters<ExtensionAPI["registerCommand"]>[1] | undefined;
let gets = 0;
Expand Down Expand Up @@ -997,16 +998,18 @@ test("media picker and listings show derived defaults without footer output or u
setStatus() {
footerWrites++;
},
async custom(factory: Parameters<ExtensionContext["ui"]["custom"]>[0]) {
async custom(
factory: Parameters<ExtensionContext["ui"]["custom"]>[0],
options?: Parameters<ExtensionContext["ui"]["custom"]>[1],
) {
const picker = await factory(
{ terminal: { rows: 14 }, requestRender() {} } as never,
theme,
new KeybindingsManager(TUI_KEYBINDINGS) as never,
() => {},
);
const rendered = picker.render(200).join("\n");
assert.match(rendered, /gpt-image-2.6-sunburst.*automatic default/);
assert.match(rendered, /gpt-image-2.6-sunburst.*selected/);
// The command reports errors thrown here, so the test asserts after the command returns.
opened = { rendered: picker.render(200).join("\n"), options };
return undefined;
},
},
Expand All @@ -1027,6 +1030,11 @@ test("media picker and listings show derived defaults without footer output or u
assert.match(output.at(-1) ?? "", /gpt-image-2.6-sunburst.*automatic default/);
}
await command.handler("", { ...ctx, mode: "tui" });
assert.ok(opened);
assert.match(opened.rendered, /gpt-image-2.6-sunburst.*automatic default/);
assert.match(opened.rendered, /gpt-image-2.6-sunburst.*selected/);
// Terminals that show program status report Pi as waiting for input while the picker is open.
assert.equal(opened.options?.blocked?.kind, "question");
await command.handler(`select ${video}`, ctx);
assert.deepEqual(readMediaDefaults(config, ctx), { video });
await command.handler("clear video", ctx);
Expand Down
Loading
Loading