@oh-my-pi/pi-ai 18.6.1 → 18.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -38
- package/README.md +15 -0
- package/dist/types/auth/types.d.ts +4 -0
- package/dist/types/auth-broker/refresher.d.ts +6 -1
- package/dist/types/error/flags.d.ts +15 -0
- package/dist/types/images/index.d.ts +2 -3
- package/dist/types/providers/anthropic.d.ts +15 -2
- package/dist/types/providers/google-auth.d.ts +4 -1
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-responses-wire.d.ts +0 -54
- package/dist/types/providers/openai-responses.d.ts +32 -3
- package/dist/types/providers/openai-shared.d.ts +4 -1
- package/dist/types/registry/oauth/cursor.d.ts +9 -1
- package/dist/types/registry/oauth/index.d.ts +2 -0
- package/dist/types/registry/oauth/snowflake.d.ts +3 -0
- package/dist/types/registry/snowflake.d.ts +4 -0
- package/dist/types/types.d.ts +72 -13
- package/dist/types/utils/idle-iterator.d.ts +11 -0
- package/dist/types/utils.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/cascade.ts +6 -2
- package/src/auth/health.ts +1 -0
- package/src/auth/select.ts +3 -1
- package/src/auth/types.ts +4 -0
- package/src/auth-broker/refresher.ts +8 -1
- package/src/error/flags.ts +22 -0
- package/src/images/index.ts +3 -17
- package/src/providers/anthropic.ts +111 -45
- package/src/providers/devin.ts +3 -1
- package/src/providers/factory-droid.ts +2 -0
- package/src/providers/gitlab-duo.ts +1 -0
- package/src/providers/google-auth.ts +10 -3
- package/src/providers/mock.ts +4 -0
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-codex-responses.ts +37 -38
- package/src/providers/openai-completions.ts +3 -0
- package/src/providers/openai-responses-wire.ts +0 -54
- package/src/providers/openai-responses.ts +349 -6
- package/src/providers/openai-shared.ts +20 -9
- package/src/providers/pi-native-server.ts +1 -0
- package/src/registry/hooks/custom.ts +2 -0
- package/src/registry/hooks/env.ts +2 -5
- package/src/registry/oauth/cursor.ts +57 -3
- package/src/registry/oauth/index.ts +5 -0
- package/src/registry/oauth/snowflake.ts +183 -0
- package/src/registry/registry.ts +2 -0
- package/src/registry/snowflake.ts +149 -0
- package/src/stream.ts +35 -33
- package/src/types.ts +93 -14
- package/src/usage/claude.ts +3 -0
- package/src/usage/cursor.ts +14 -25
- package/src/usage/minimax-code.ts +8 -1
- package/src/utils/idle-iterator.ts +80 -35
- package/src/utils/thinking-loop.ts +18 -10
- package/src/utils.ts +19 -4
package/dist/types/utils.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export declare function stripOpenAIResponsesOutputOnlyStatusesForReplay<TItem ex
|
|
|
34
34
|
type?: unknown;
|
|
35
35
|
status?: unknown;
|
|
36
36
|
}>(items: TItem[]): TItem[];
|
|
37
|
+
/** Clamp replayed images without changing item identities or other native replay fields. */
|
|
38
|
+
export declare function clampOpenAIResponsesImageDetailForReplay<TItem extends {
|
|
39
|
+
type?: unknown;
|
|
40
|
+
detail?: unknown;
|
|
41
|
+
content?: unknown;
|
|
42
|
+
}>(items: TItem[], supportsImageDetailOriginal: boolean): TItem[];
|
|
37
43
|
export declare function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>, options?: OpenAIResponsesReplaySanitizeOptions): ResponseInput;
|
|
38
44
|
/** Strip reasoning IDs whose only linked native output is a computer call that will be demoted. */
|
|
39
45
|
export declare function stripOpenAIResponsesComputerLinkedReasoningIdsForReplay(items: ResponseInput): ResponseInput;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.7.0",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.
|
|
158
|
+
"@oh-my-pi/omptype": "18.7.0",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.7.0",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.7.0",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.7.0",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.7.0"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
package/src/auth/cascade.ts
CHANGED
|
@@ -356,7 +356,9 @@ export class KeyCascade implements KeysApi {
|
|
|
356
356
|
credential => credential.source === "login" && !this.isKeylessFallback(provider, credential),
|
|
357
357
|
);
|
|
358
358
|
if (loginApiKeySelection) {
|
|
359
|
-
|
|
359
|
+
if (options?.recordAffinity !== false) {
|
|
360
|
+
this.#deps.affinity.record(provider, sessionId, "api_key", loginApiKeySelection.index);
|
|
361
|
+
}
|
|
360
362
|
const credentialId = onCredentialId
|
|
361
363
|
? this.#deps.pool.entries(provider)[loginApiKeySelection.index]?.id
|
|
362
364
|
: undefined;
|
|
@@ -382,7 +384,9 @@ export class KeyCascade implements KeysApi {
|
|
|
382
384
|
credential => credential.source !== "login",
|
|
383
385
|
);
|
|
384
386
|
if (apiKeySelection) {
|
|
385
|
-
|
|
387
|
+
if (options?.recordAffinity !== false) {
|
|
388
|
+
this.#deps.affinity.record(provider, sessionId, "api_key", apiKeySelection.index);
|
|
389
|
+
}
|
|
386
390
|
const credentialId = onCredentialId ? this.#deps.pool.entries(provider)[apiKeySelection.index]?.id : undefined;
|
|
387
391
|
const apiKey = await this.#deps.overrides.resolve(apiKeySelection.credential.key);
|
|
388
392
|
if (apiKey !== undefined && credentialId !== undefined) onCredentialId?.(credentialId);
|
package/src/auth/health.ts
CHANGED
|
@@ -283,6 +283,7 @@ export class CredentialHealth implements HealthApi {
|
|
|
283
283
|
const results: CredentialHealthResult[] = [];
|
|
284
284
|
for (const row of stored) {
|
|
285
285
|
options?.signal?.throwIfAborted();
|
|
286
|
+
if (options?.excludeProviders?.has(row.provider)) continue;
|
|
286
287
|
const base: CredentialHealthResult = {
|
|
287
288
|
id: row.id,
|
|
288
289
|
provider: row.provider,
|
package/src/auth/select.ts
CHANGED
|
@@ -1055,7 +1055,9 @@ export class CredentialSelector {
|
|
|
1055
1055
|
}
|
|
1056
1056
|
}
|
|
1057
1057
|
this.#deps.pool.noteBearer(provider, result.apiKey, credentialId);
|
|
1058
|
-
|
|
1058
|
+
if (options?.recordAffinity !== false) {
|
|
1059
|
+
this.#deps.affinity.record(provider, sessionId, "oauth", selection.index);
|
|
1060
|
+
}
|
|
1059
1061
|
return { apiKey: result.apiKey, credential: updated, credentialId };
|
|
1060
1062
|
} catch (error) {
|
|
1061
1063
|
const errorMsg = String(error);
|
package/src/auth/types.ts
CHANGED
|
@@ -257,6 +257,8 @@ export interface CheckCredentialsOptions {
|
|
|
257
257
|
completionProbe?: CompletionProbe;
|
|
258
258
|
/** Per-credential completion probe timeout (ms). Defaults to `timeoutMs`. */
|
|
259
259
|
completionTimeoutMs?: number;
|
|
260
|
+
/** Providers whose credentials are skipped entirely (not probed, not reported). */
|
|
261
|
+
excludeProviders?: ReadonlySet<string>;
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -487,6 +489,8 @@ export type AuthApiKeyOptions = {
|
|
|
487
489
|
forceRefresh?: boolean;
|
|
488
490
|
/** Explicit provider-401 recovery; generic force refreshes leave this unset. */
|
|
489
491
|
refreshReason?: OAuthRefreshReason;
|
|
492
|
+
/** When false, select as `sessionId` would without recording the choice as that session's sticky credential. */
|
|
493
|
+
recordAffinity?: boolean;
|
|
490
494
|
};
|
|
491
495
|
|
|
492
496
|
/** Non-secret identity bound to the OAuth credential selected for one request attempt. */
|
|
@@ -75,7 +75,12 @@ export class AuthBrokerRefresher {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Run one sweep. Exposed for tests.
|
|
80
|
+
*
|
|
81
|
+
* Never rejects: callers fire it unawaited from a timer, so a failed sweep
|
|
82
|
+
* (e.g. an unreadable credential store) is logged and retried next interval.
|
|
83
|
+
*/
|
|
79
84
|
async tick(): Promise<void> {
|
|
80
85
|
if (this.#running) return;
|
|
81
86
|
this.#running = true;
|
|
@@ -94,6 +99,8 @@ export class AuthBrokerRefresher {
|
|
|
94
99
|
targets.push(entry.id);
|
|
95
100
|
}
|
|
96
101
|
await Promise.all(targets.map(id => this.#refreshOne(id)));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.warn("auth-broker refresh sweep failed", { error: String(error) });
|
|
97
104
|
} finally {
|
|
98
105
|
this.#running = false;
|
|
99
106
|
this.#nextSweepAt = this.#now() + this.#refreshIntervalMs;
|
package/src/error/flags.ts
CHANGED
|
@@ -187,6 +187,25 @@ export function isResponsesRequestBodyReadTimeout(message: {
|
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Codex code for the experimental native turn lane refusing a mid-response
|
|
192
|
+
* `response.steer`; the server then drops the in-flight response and closes
|
|
193
|
+
* the socket.
|
|
194
|
+
*/
|
|
195
|
+
export const CODEX_NATIVE_LANE_STEER_REJECTED_CODE = "unsupported_native_inflight_message";
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A Codex turn the native turn lane dropped because omp steered it. The
|
|
199
|
+
* rejection answers our own `response.steer`, not the model's health: the
|
|
200
|
+
* provider stops steering the session, so the same model replays cleanly.
|
|
201
|
+
*/
|
|
202
|
+
export function isCodexSteerRejection(message: { api?: Api; errorMessage?: string }): boolean {
|
|
203
|
+
return (
|
|
204
|
+
message.api === "openai-codex-responses" &&
|
|
205
|
+
message.errorMessage?.includes(`code=${CODEX_NATIVE_LANE_STEER_REJECTED_CODE}`) === true
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
190
209
|
export const TRANSIENT_TRANSPORT_PATTERN =
|
|
191
210
|
/\b(?:no[_ -]?capacity|(?:high|peak)[ _-]?demand|(?:at|over|insufficient)[ _-]?capacity|capacity[ _-]?(?:exceeded|exhausted)|peak[ _-]?load)\b|overloaded|provider.?returned.?error|rate.?limit|too many requests|auth-gateway\s+5\d{2}(?=[:\s]|$)|\b(?:429|500|502|503|504)\b|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|unable.?to.?connect\.\s*is the computer able to access the url\?|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|nghttp2_(?:internal_error|refused_stream)|stream closed with error code nghttp2_(?:internal_error|refused_stream)|malformed.?function.?call/i;
|
|
192
211
|
const AUTH_FAILURE_PATTERN =
|
|
@@ -586,6 +605,9 @@ function classifyText(
|
|
|
586
605
|
if ((api === "openai-responses" || api === "openai-codex-responses") && isStaleResponsesText(errorMessage)) {
|
|
587
606
|
kinds |= Flag.StaleResponsesItem;
|
|
588
607
|
}
|
|
608
|
+
// Retryable like the provider's own classification of the code, so a
|
|
609
|
+
// message reclassified from its text alone still retries.
|
|
610
|
+
if (isCodexSteerRejection({ api, errorMessage })) kinds |= Flag.Transient;
|
|
589
611
|
|
|
590
612
|
// Fireworks mid-generation NaN 400 is a model-side decode fault, not a bad
|
|
591
613
|
// request; a byte-identical replay succeeds, so treat it as transient.
|
package/src/images/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Api, IMAGE_GENERATION_APIS, type ImageGenerationApi, type Model } from "@oh-my-pi/pi-catalog/types";
|
|
2
2
|
import * as AIError from "../error";
|
|
3
3
|
import { generateAntigravityImage } from "./google-antigravity";
|
|
4
4
|
import { generateGoogleImage } from "./google-generative-ai";
|
|
@@ -14,25 +14,11 @@ export * from "./openai-images";
|
|
|
14
14
|
export * from "./openrouter-images";
|
|
15
15
|
export * from "./types";
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
export type ImageGenerationApi =
|
|
19
|
-
| "openai-images"
|
|
20
|
-
| "openrouter-images"
|
|
21
|
-
| "google-generative-ai"
|
|
22
|
-
| "google-gemini-cli"
|
|
23
|
-
| "openai-responses"
|
|
24
|
-
| "openai-codex-responses";
|
|
17
|
+
export type { ImageGenerationApi } from "@oh-my-pi/pi-catalog/types";
|
|
25
18
|
|
|
26
19
|
/** Whether a catalog API generates images through one of the pi-ai image clients. */
|
|
27
20
|
export function isImageGenerationApi(api: Api): api is ImageGenerationApi {
|
|
28
|
-
return (
|
|
29
|
-
api === "openai-images" ||
|
|
30
|
-
api === "openrouter-images" ||
|
|
31
|
-
api === "google-generative-ai" ||
|
|
32
|
-
api === "google-gemini-cli" ||
|
|
33
|
-
api === "openai-responses" ||
|
|
34
|
-
api === "openai-codex-responses"
|
|
35
|
-
);
|
|
21
|
+
return (IMAGE_GENERATION_APIS as readonly Api[]).includes(api);
|
|
36
22
|
}
|
|
37
23
|
|
|
38
24
|
/** Generate (or edit, when `request.inputImages` is set) images through the transport selected by the model's `api`. */
|
|
@@ -3,6 +3,7 @@ import { scheduler } from "node:timers/promises";
|
|
|
3
3
|
import * as tls from "node:tls";
|
|
4
4
|
import { isAnthropicSigningProxyUrl, isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
|
|
5
5
|
import { hostMatchesUrl, isVertexRawPredictUrl } from "@oh-my-pi/pi-catalog/hosts";
|
|
6
|
+
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
6
7
|
import { mapEffortToAnthropicAdaptiveEffort } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
7
8
|
import { calculateCost, getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
|
8
9
|
import { isAnthropicOAuthToken } from "@oh-my-pi/pi-catalog/utils";
|
|
@@ -21,7 +22,9 @@ import * as AIError from "../error";
|
|
|
21
22
|
import { parseToolCallArguments } from "../utils/tool-call-arguments";
|
|
22
23
|
import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
|
|
23
24
|
import type {
|
|
25
|
+
AnthropicCompactionFiles,
|
|
24
26
|
AnthropicCompactionPayload,
|
|
27
|
+
AnthropicCompactionRequest,
|
|
25
28
|
AnthropicFallbackContent,
|
|
26
29
|
AnthropicMessagePayload,
|
|
27
30
|
AnthropicOutputEffort,
|
|
@@ -646,7 +649,7 @@ const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
|
|
646
649
|
function createClaudeBillingHeader(firstUserMessageText: string): string {
|
|
647
650
|
// Fingerprint: SHA256(salt + msg[4] + msg[7] + msg[20] + version)[:3]
|
|
648
651
|
// Matches CC's computeFingerprint in utils/fingerprint.ts.
|
|
649
|
-
// Uses chars from the first user
|
|
652
|
+
// Uses chars from the first conversational wire user turn (not the system prompt).
|
|
650
653
|
const k = [4, 7, 20].map(i => firstUserMessageText[i] ?? "0").join("");
|
|
651
654
|
const version = getClaudeCodeVersion();
|
|
652
655
|
const versionSuffix = Bun.SHA256.hash(`59cf53e54c78${k}${version}`, "hex").slice(0, 3);
|
|
@@ -3498,7 +3501,7 @@ export type AnthropicSystemBlock = {
|
|
|
3498
3501
|
type SystemBlockOptions = {
|
|
3499
3502
|
includeClaudeCodeInstruction?: boolean;
|
|
3500
3503
|
extraInstructions?: string[];
|
|
3501
|
-
/** Text of the first user
|
|
3504
|
+
/** Text of the first conversational wire user turn, used as the billing fingerprint seed. */
|
|
3502
3505
|
firstUserMessageText?: string;
|
|
3503
3506
|
/** Cache lifetime shared by the OAuth system breakpoint and later message breakpoints. */
|
|
3504
3507
|
cacheControl?: AnthropicCacheControl;
|
|
@@ -4185,16 +4188,19 @@ function resolveAnthropicAdaptiveEffort(
|
|
|
4185
4188
|
return mapEffortToAnthropicAdaptiveEffort(model, requestedEffort);
|
|
4186
4189
|
}
|
|
4187
4190
|
|
|
4188
|
-
function
|
|
4191
|
+
function extractClaudeCodeFirstWireUserMessageText(messages: readonly Message[]): string {
|
|
4189
4192
|
for (const message of messages) {
|
|
4190
|
-
if (message.role !== "user") continue;
|
|
4193
|
+
if ((message.role !== "user" && message.role !== "developer") || anthropicControlOf(message)) continue;
|
|
4191
4194
|
const { content } = message;
|
|
4192
|
-
if (typeof content === "string")
|
|
4193
|
-
|
|
4195
|
+
if (typeof content === "string") {
|
|
4196
|
+
if (content.trim()) return content;
|
|
4197
|
+
continue;
|
|
4198
|
+
}
|
|
4199
|
+
if (!Array.isArray(content)) continue;
|
|
4194
4200
|
for (const block of content) {
|
|
4195
|
-
if (block.type === "text") return block.text;
|
|
4201
|
+
if (block.type === "text" && block.text.trim()) return block.text;
|
|
4196
4202
|
}
|
|
4197
|
-
return "";
|
|
4203
|
+
if (content.some(block => block.type === "image")) return "";
|
|
4198
4204
|
}
|
|
4199
4205
|
return "";
|
|
4200
4206
|
}
|
|
@@ -4306,11 +4312,16 @@ function diffAnthropicActiveTools(
|
|
|
4306
4312
|
* at the tail. Records at a different index than they were written at are
|
|
4307
4313
|
* rewritten history: they keep the declaration but emit no controls, so the
|
|
4308
4314
|
* net change from the declared baseline lands at the first live position.
|
|
4315
|
+
* Without `ownChange` (an on-demand compaction request, which ends inside the
|
|
4316
|
+
* conversation) the tail control is left out: there it would sit between the
|
|
4317
|
+
* summarized prefix and the retained turns, where live requests never sent
|
|
4318
|
+
* it, and invalidate their signed thinking. The next live turn sends it.
|
|
4309
4319
|
*/
|
|
4310
4320
|
function planAnthropicToolControls(
|
|
4311
4321
|
context: Context,
|
|
4312
4322
|
records: readonly AnthropicControlRecord[],
|
|
4313
4323
|
enabled: boolean,
|
|
4324
|
+
ownChange: boolean,
|
|
4314
4325
|
): { tools: Tool[] | undefined; inserts: AnthropicControlInsert[]; record: AnthropicToolControls | undefined } {
|
|
4315
4326
|
if (!enabled || !context.tools) return { tools: context.tools, inserts: [], record: undefined };
|
|
4316
4327
|
const definitions = new Map<string, Tool>();
|
|
@@ -4353,7 +4364,7 @@ function planAnthropicToolControls(
|
|
|
4353
4364
|
if (toolChanges.length > 0) inserts.push({ index: record.index, spec: { toolChanges } });
|
|
4354
4365
|
previous = record.tools.active;
|
|
4355
4366
|
}
|
|
4356
|
-
const toolChanges = diffAnthropicActiveTools(previous, activeNames, declaredSet);
|
|
4367
|
+
const toolChanges = ownChange ? diffAnthropicActiveTools(previous, activeNames, declaredSet) : [];
|
|
4357
4368
|
if (toolChanges.length > 0) inserts.push({ index: context.messages.length, spec: { toolChanges } });
|
|
4358
4369
|
}
|
|
4359
4370
|
|
|
@@ -4380,7 +4391,8 @@ function planAnthropicToolControls(
|
|
|
4380
4391
|
* restore, so a request without an explicit effort keeps the level in force.
|
|
4381
4392
|
* Records at a different index than they were written at are rewritten history:
|
|
4382
4393
|
* they keep the top-level effort but emit no controls, so the net change lands
|
|
4383
|
-
* at the first live position.
|
|
4394
|
+
* at the first live position. Without `ownChange` (an on-demand compaction
|
|
4395
|
+
* request) `current` is not applied: the request runs at the effort in force.
|
|
4384
4396
|
*/
|
|
4385
4397
|
function planAnthropicEffortControls(
|
|
4386
4398
|
current: AnthropicOutputEffort | undefined,
|
|
@@ -4388,6 +4400,7 @@ function planAnthropicEffortControls(
|
|
|
4388
4400
|
records: readonly AnthropicControlRecord[],
|
|
4389
4401
|
enabled: boolean,
|
|
4390
4402
|
compactionReplay: AnthropicCompactionReplay | undefined,
|
|
4403
|
+
ownChange: boolean,
|
|
4391
4404
|
): {
|
|
4392
4405
|
topLevel: AnthropicOutputEffort | undefined;
|
|
4393
4406
|
inserts: AnthropicControlInsert[];
|
|
@@ -4418,7 +4431,7 @@ function planAnthropicEffortControls(
|
|
|
4418
4431
|
}
|
|
4419
4432
|
tail = recorded ?? tail;
|
|
4420
4433
|
}
|
|
4421
|
-
if (current !== undefined && current !== tail) {
|
|
4434
|
+
if (ownChange && current !== undefined && current !== tail) {
|
|
4422
4435
|
inserts.push({
|
|
4423
4436
|
index: anthropicEffortInsertIndex(messages, messages.length, compactionReplay),
|
|
4424
4437
|
spec: { toolChanges: [], effort: current },
|
|
@@ -4428,6 +4441,32 @@ function planAnthropicEffortControls(
|
|
|
4428
4441
|
return { topLevel, inserts, record: { topLevel: topLevel ?? null, tail: tail ?? null } };
|
|
4429
4442
|
}
|
|
4430
4443
|
|
|
4444
|
+
/**
|
|
4445
|
+
* The effort whose thinking allowance an on-demand compaction request over
|
|
4446
|
+
* `messages` gets. It sends no effort change of its own, so once an earlier
|
|
4447
|
+
* request recorded an effort, the one it left in force applies, even when
|
|
4448
|
+
* `requested` differs or is off. When that request named none, the API's
|
|
4449
|
+
* per-model default applies, which is not known here: the request gets the
|
|
4450
|
+
* largest effort's allowance (`max_tokens` is a cap, not spend). Without a
|
|
4451
|
+
* recorded effort the request sets its own.
|
|
4452
|
+
*/
|
|
4453
|
+
export function resolveAnthropicCompactionEffort(
|
|
4454
|
+
model: Model<"anthropic-messages">,
|
|
4455
|
+
messages: readonly Message[],
|
|
4456
|
+
requested: Effort | undefined,
|
|
4457
|
+
): Effort | undefined {
|
|
4458
|
+
if (model.compat.supportsPerMessageEffort !== true) return requested;
|
|
4459
|
+
const records = collectAnthropicControlRecords(messages);
|
|
4460
|
+
if (!records.some(record => record.controls.effort)) return requested;
|
|
4461
|
+
const inForce = planAnthropicEffortControls(undefined, messages, records, true, undefined, false).record?.tail;
|
|
4462
|
+
if (!inForce) return requested && (model.thinking?.efforts.at(-1) ?? requested);
|
|
4463
|
+
if (requested && mapEffortToAnthropicAdaptiveEffort(model, requested) === inForce) return requested;
|
|
4464
|
+
return (
|
|
4465
|
+
model.thinking?.efforts?.findLast(effort => mapEffortToAnthropicAdaptiveEffort(model, effort) === inForce) ??
|
|
4466
|
+
requested
|
|
4467
|
+
);
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4431
4470
|
/**
|
|
4432
4471
|
* Splice control markers into `messages`. Inserts sharing an index come from
|
|
4433
4472
|
* one request and merge into one control: tool changes first, then the effort.
|
|
@@ -4593,7 +4632,7 @@ function buildParams(
|
|
|
4593
4632
|
// Pre-compute system blocks so they occupy the right slot in the serialized body.
|
|
4594
4633
|
const shouldInjectClaudeCodeInstruction = isOAuthToken && model.compat.injectClaudeCodeInstruction !== false;
|
|
4595
4634
|
const firstUserMessageText = shouldInjectClaudeCodeInstruction
|
|
4596
|
-
?
|
|
4635
|
+
? extractClaudeCodeFirstWireUserMessageText(context.messages)
|
|
4597
4636
|
: "";
|
|
4598
4637
|
const systemBlocks = buildAnthropicSystemBlocks(context.systemPrompt, {
|
|
4599
4638
|
includeClaudeCodeInstruction: shouldInjectClaudeCodeInstruction,
|
|
@@ -4601,6 +4640,9 @@ function buildParams(
|
|
|
4601
4640
|
cacheControl,
|
|
4602
4641
|
});
|
|
4603
4642
|
|
|
4643
|
+
// An on-demand compaction request ends inside the conversation: it gets
|
|
4644
|
+
// nothing live requests lack there, so it makes no tool or effort change.
|
|
4645
|
+
const compactionRequest = compactionSupported ? options?.anthropicCompaction : undefined;
|
|
4604
4646
|
// Controls earlier requests recorded on their responses fix the declared
|
|
4605
4647
|
// tools, the top-level effort and every control message in between.
|
|
4606
4648
|
const records = collectAnthropicControlRecords(context.messages);
|
|
@@ -4608,6 +4650,7 @@ function buildParams(
|
|
|
4608
4650
|
context,
|
|
4609
4651
|
records,
|
|
4610
4652
|
model.compat.supportsMidConversationToolChanges === true,
|
|
4653
|
+
!compactionRequest,
|
|
4611
4654
|
);
|
|
4612
4655
|
|
|
4613
4656
|
// Pre-compute tools.
|
|
@@ -4750,7 +4793,6 @@ function buildParams(
|
|
|
4750
4793
|
// A new on-demand compaction request cannot carry context_management.
|
|
4751
4794
|
// Later turns carrying its signed block may keep clear_thinking as usual.
|
|
4752
4795
|
// Persisted encrypted threshold blocks alone need a legacy replay edit.
|
|
4753
|
-
const compactionRequest = compactionSupported ? options?.anthropicCompaction : undefined;
|
|
4754
4796
|
const signedReplay = compactionSupported && contextReplaysAnthropicCompaction(context.messages, model, "signed");
|
|
4755
4797
|
const legacyReplay =
|
|
4756
4798
|
compactionSupported && !signedReplay && contextReplaysAnthropicCompaction(context.messages, model, "legacy");
|
|
@@ -4776,6 +4818,7 @@ function buildParams(
|
|
|
4776
4818
|
records,
|
|
4777
4819
|
model.compat.supportsPerMessageEffort === true,
|
|
4778
4820
|
compactionReplay,
|
|
4821
|
+
!compactionRequest,
|
|
4779
4822
|
);
|
|
4780
4823
|
// `between_tools` returns a 400 at `xhigh`/`max` effort, and the effort in
|
|
4781
4824
|
// force from earlier turns outlives a thinking toggle. Fall back to the
|
|
@@ -4794,6 +4837,7 @@ function buildParams(
|
|
|
4794
4837
|
dropAllThinking: dropAllThinking || stripThinkingHistory,
|
|
4795
4838
|
droppedThinkingBlocks,
|
|
4796
4839
|
credentialId: options?.credentialId,
|
|
4840
|
+
compactionRequest,
|
|
4797
4841
|
},
|
|
4798
4842
|
);
|
|
4799
4843
|
// Anchor the stable tools+system head so it stays cached across turns; the
|
|
@@ -5053,6 +5097,8 @@ export function convertAnthropicMessages(
|
|
|
5053
5097
|
dropAllThinking?: boolean;
|
|
5054
5098
|
droppedThinkingBlocks?: ReadonlySet<string>;
|
|
5055
5099
|
credentialId?: number;
|
|
5100
|
+
/** On-demand compaction request: it ends inside the conversation, so it gets nothing live requests lack there. */
|
|
5101
|
+
compactionRequest?: AnthropicCompactionRequest;
|
|
5056
5102
|
},
|
|
5057
5103
|
): AnthropicMessageParam[] {
|
|
5058
5104
|
// Indices of params emitted from `developer` messages. After the main pass,
|
|
@@ -5064,21 +5110,33 @@ export function convertAnthropicMessages(
|
|
|
5064
5110
|
// and the developer upgrade below look through them: they were inserted
|
|
5065
5111
|
// by this provider, not authored in the conversation.
|
|
5066
5112
|
const controlParams = new Set<AnthropicMessageParam>();
|
|
5067
|
-
// Harness file metadata queued behind a replayed compaction block
|
|
5068
|
-
//
|
|
5069
|
-
//
|
|
5070
|
-
//
|
|
5071
|
-
// results
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5113
|
+
// Harness file metadata queued behind a replayed compaction block, oldest
|
|
5114
|
+
// replay point first. Messages kept after a summary must reach the API
|
|
5115
|
+
// exactly as they were sent before, or their signed thinking no longer
|
|
5116
|
+
// matches its prefix; each entry therefore waits for the first message
|
|
5117
|
+
// created after its replay point (past any tool results that close an open
|
|
5118
|
+
// tool_use turn), or the end of the list.
|
|
5119
|
+
let pendingCompactionFiles: AnthropicCompactionFiles[] = [];
|
|
5120
|
+
const pushCompactionFiles = (filesText: string): void => {
|
|
5121
|
+
if (filesText.trim().length === 0) return;
|
|
5122
|
+
// The payload bypassed the `transformMessages` redaction pass, so
|
|
5123
|
+
// the metadata takes the same credential redaction here that the
|
|
5124
|
+
// dropped message text received there.
|
|
5125
|
+
params.push({ role: "user", content: redactSensitiveCredentials(filesText) });
|
|
5126
|
+
};
|
|
5127
|
+
const flushCompactionFiles = (before: number): void => {
|
|
5128
|
+
const due = pendingCompactionFiles.filter(files => files.after < before);
|
|
5129
|
+
pendingCompactionFiles = pendingCompactionFiles.filter(files => files.after >= before);
|
|
5130
|
+
for (const files of due) pushCompactionFiles(files.text);
|
|
5131
|
+
};
|
|
5132
|
+
// Summaries without `exactTail` replay their metadata after the first param
|
|
5133
|
+
// boundary past the block (clear of the fold and of an open tool_use turn):
|
|
5134
|
+
// thinking created after them was signed against that layout.
|
|
5135
|
+
let legacyCompactionFiles: string | undefined;
|
|
5136
|
+
const flushLegacyCompactionFiles = (): void => {
|
|
5137
|
+
if (legacyCompactionFiles === undefined) return;
|
|
5138
|
+
pushCompactionFiles(legacyCompactionFiles);
|
|
5139
|
+
legacyCompactionFiles = undefined;
|
|
5082
5140
|
};
|
|
5083
5141
|
|
|
5084
5142
|
const transformedMessages = transformMessages(
|
|
@@ -5118,17 +5176,23 @@ export function convertAnthropicMessages(
|
|
|
5118
5176
|
params.push(compactionParam);
|
|
5119
5177
|
// The block carries the verbatim API summary, so the message text
|
|
5120
5178
|
// (which holds the harness file lists) would be dropped with it.
|
|
5121
|
-
// Queue the file metadata
|
|
5122
|
-
//
|
|
5123
|
-
|
|
5124
|
-
if (
|
|
5125
|
-
pendingCompactionFiles
|
|
5179
|
+
// Queue the file metadata past the retained tail (see above); the
|
|
5180
|
+
// summary message carries the compaction's commit time.
|
|
5181
|
+
const { filesText, retainedFiles, exactTail } = msg.providerPayload;
|
|
5182
|
+
if (exactTail) {
|
|
5183
|
+
pendingCompactionFiles = [
|
|
5184
|
+
...(retainedFiles ?? []),
|
|
5185
|
+
...(filesText !== undefined ? [{ text: filesText, after: msg.timestamp }] : []),
|
|
5186
|
+
].sort((a, b) => a.after - b.after);
|
|
5187
|
+
} else {
|
|
5188
|
+
pendingCompactionFiles = [];
|
|
5189
|
+
legacyCompactionFiles = filesText;
|
|
5126
5190
|
}
|
|
5127
5191
|
continue;
|
|
5128
5192
|
}
|
|
5193
|
+
if (msg.role !== "toolResult") flushCompactionFiles(msg.timestamp);
|
|
5129
5194
|
if (msg.role === "user" || msg.role === "developer") {
|
|
5130
|
-
|
|
5131
|
-
flushCompactionFiles();
|
|
5195
|
+
flushLegacyCompactionFiles();
|
|
5132
5196
|
const payload =
|
|
5133
5197
|
msg.role === "developer" && msg.providerPayload?.type === "anthropicMessage"
|
|
5134
5198
|
? msg.providerPayload
|
|
@@ -5328,12 +5392,7 @@ export function convertAnthropicMessages(
|
|
|
5328
5392
|
};
|
|
5329
5393
|
copyPerCallContextMessage(assistantParam, msg);
|
|
5330
5394
|
params.push(assistantParam);
|
|
5331
|
-
|
|
5332
|
-
// their results must follow the turn contiguously, so the metadata
|
|
5333
|
-
// waits for the merged result message (or the end of the list).
|
|
5334
|
-
if (!blocks.some(block => block.type === "tool_use")) {
|
|
5335
|
-
flushCompactionFiles();
|
|
5336
|
-
}
|
|
5395
|
+
if (!blocks.some(block => block.type === "tool_use")) flushLegacyCompactionFiles();
|
|
5337
5396
|
} else if (msg.role === "toolResult") {
|
|
5338
5397
|
// Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint
|
|
5339
5398
|
const toolResults: ContentBlockParam[] = [];
|
|
@@ -5369,9 +5428,7 @@ export function convertAnthropicMessages(
|
|
|
5369
5428
|
|
|
5370
5429
|
// Add a single user message with all tool results
|
|
5371
5430
|
params.push(toolResultParam);
|
|
5372
|
-
|
|
5373
|
-
// metadata can follow without splitting the pairing.
|
|
5374
|
-
flushCompactionFiles();
|
|
5431
|
+
flushLegacyCompactionFiles();
|
|
5375
5432
|
}
|
|
5376
5433
|
}
|
|
5377
5434
|
|
|
@@ -5468,9 +5525,18 @@ export function convertAnthropicMessages(
|
|
|
5468
5525
|
params.splice(previous + 1, 0, { role: "user", content: "Continue." });
|
|
5469
5526
|
i = previous + 1;
|
|
5470
5527
|
}
|
|
5528
|
+
if (opts?.compactionRequest) {
|
|
5529
|
+
// Metadata due before `filesDueBefore` ends the summarized range; the
|
|
5530
|
+
// rest replays with the retained tail and is carried by the new summary
|
|
5531
|
+
// instead. Legacy metadata still pending here belongs after the first
|
|
5532
|
+
// retained turn, outside this request.
|
|
5533
|
+
flushCompactionFiles(opts.compactionRequest.filesDueBefore ?? Number.POSITIVE_INFINITY);
|
|
5534
|
+
return params;
|
|
5535
|
+
}
|
|
5471
5536
|
// A trailing compaction summary leaves its file metadata queued; emit it
|
|
5472
5537
|
// before the prefill check so the list ends the request as a user turn.
|
|
5473
|
-
flushCompactionFiles();
|
|
5538
|
+
flushCompactionFiles(Number.POSITIVE_INFINITY);
|
|
5539
|
+
flushLegacyCompactionFiles();
|
|
5474
5540
|
const last = skipControls(params.length, -1);
|
|
5475
5541
|
if (params[last]?.role === "assistant") {
|
|
5476
5542
|
params.splice(last + 1, 0, { role: "user", content: "Continue." });
|
package/src/providers/devin.ts
CHANGED
|
@@ -236,7 +236,9 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
236
236
|
assignment = await assignDevinModel(model, turn, chatBaseUrl, fetchImpl, options?.signal);
|
|
237
237
|
output.upstreamModel = assignment.modelUid;
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
let request = buildDevinChatRequest(model, context, options, turn, assignment);
|
|
240
|
+
const replacementRequest = await options?.onPayload?.(request, model);
|
|
241
|
+
if (replacementRequest !== undefined) request = replacementRequest as typeof request;
|
|
240
242
|
const reqBytes = toBinary(GetChatMessageRequestSchema, request);
|
|
241
243
|
const gz = gzipSync(reqBytes);
|
|
242
244
|
logger.debug("devin: sending chat request", {
|
|
@@ -366,6 +366,7 @@ type ForwardedOptions = Pick<
|
|
|
366
366
|
| "streamIdleTimeoutMs"
|
|
367
367
|
| "streamFirstEventTimeoutMs"
|
|
368
368
|
| "maxTokens"
|
|
369
|
+
| "waitForTerminalDrain"
|
|
369
370
|
> & { apiKey: string; headers: Record<string, string> };
|
|
370
371
|
|
|
371
372
|
/** One resolved request attempt, shared by every wire encoder. */
|
|
@@ -580,6 +581,7 @@ export const streamFactoryDroid: StreamFunction<"factory-droid-agent"> = (
|
|
|
580
581
|
streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
|
|
581
582
|
streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
|
|
582
583
|
maxTokens: options?.maxTokens ?? scope.model.maxTokens ?? undefined,
|
|
584
|
+
waitForTerminalDrain: options?.waitForTerminalDrain,
|
|
583
585
|
headers: {
|
|
584
586
|
...buildIdentityHeaders({
|
|
585
587
|
upstream: scope.upstream,
|
|
@@ -250,6 +250,7 @@ export function streamGitLabDuo(
|
|
|
250
250
|
// like the azure-openai-responses mapping does.
|
|
251
251
|
disableReasoning: options.disableReasoning || options.forceReasoningOff,
|
|
252
252
|
toolChoice: options.toolChoice,
|
|
253
|
+
waitForTerminalDrain: options.waitForTerminalDrain,
|
|
253
254
|
} satisfies OpenAICompletionsOptions,
|
|
254
255
|
);
|
|
255
256
|
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Sources, in priority order:
|
|
6
6
|
* 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange)
|
|
7
7
|
* or `type: "authorized_user"` (refresh-token exchange).
|
|
8
|
-
* 2.
|
|
8
|
+
* 2. gcloud user ADC: `%APPDATA%\gcloud\application_default_credentials.json` on Windows,
|
|
9
|
+
* `~/.config/gcloud/application_default_credentials.json` elsewhere (authorized_user flow).
|
|
9
10
|
* 3. GCE / Cloud Run metadata server (`metadata.google.internal`).
|
|
10
11
|
*
|
|
11
12
|
* Tokens are cached per source key and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` before expiry
|
|
@@ -66,8 +67,14 @@ function getRefreshSkewMs(): number {
|
|
|
66
67
|
return $envpos("GOOGLE_VERTEX_REFRESH_SKEW_MS", 60_000);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
/** gcloud's user ADC file: `%APPDATA%\gcloud\…` on Windows, `~/.config/gcloud/…` elsewhere. */
|
|
71
|
+
export function userAdcPath(
|
|
72
|
+
platform: typeof process.platform = process.platform,
|
|
73
|
+
appData: string | undefined = Bun.env.APPDATA,
|
|
74
|
+
): string {
|
|
75
|
+
const gcloudDir =
|
|
76
|
+
platform === "win32" && appData ? path.join(appData, "gcloud") : path.join(os.homedir(), ".config", "gcloud");
|
|
77
|
+
return path.join(gcloudDir, "application_default_credentials.json");
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
async function readJsonFile<T>(filePath: string): Promise<T | undefined> {
|
package/src/providers/mock.ts
CHANGED
|
@@ -51,6 +51,7 @@ import type {
|
|
|
51
51
|
AssistantMessage,
|
|
52
52
|
Context,
|
|
53
53
|
Model,
|
|
54
|
+
ServiceTier,
|
|
54
55
|
SimpleStreamOptions,
|
|
55
56
|
StopDetails,
|
|
56
57
|
StopReason,
|
|
@@ -93,6 +94,8 @@ export interface MockResponse {
|
|
|
93
94
|
errorMessage?: string;
|
|
94
95
|
/** Usage stats. Missing fields default to 0; missing `cost.total` is recomputed from components. */
|
|
95
96
|
usage?: Partial<Omit<Usage, "cost">> & { cost?: Partial<Usage["cost"]> };
|
|
97
|
+
/** Service tier the mock reports serving the turn, as a real provider's response echo would. */
|
|
98
|
+
serviceTier?: ServiceTier;
|
|
96
99
|
/** Pre-set responseId. */
|
|
97
100
|
responseId?: string;
|
|
98
101
|
/** If set, the stream emits a terminal error event instead of completing. */
|
|
@@ -409,6 +412,7 @@ async function runMock(
|
|
|
409
412
|
partial.fallbackCreditHandle = response.fallbackCreditHandle;
|
|
410
413
|
partial.errorMessage = response.errorMessage;
|
|
411
414
|
partial.usage = mergeUsage(response.usage);
|
|
415
|
+
partial.serviceTier = response.serviceTier;
|
|
412
416
|
partial.duration = performance.now() - perfStart;
|
|
413
417
|
|
|
414
418
|
if (reason === "aborted" || reason === "error") {
|
|
@@ -147,6 +147,7 @@ export function streamOpenAIAnthropicShim(
|
|
|
147
147
|
toolChoice: options?.toolChoice,
|
|
148
148
|
serviceTier: options?.serviceTier,
|
|
149
149
|
disableReasoning: options?.disableReasoning,
|
|
150
|
+
waitForTerminalDrain: options?.waitForTerminalDrain,
|
|
150
151
|
});
|
|
151
152
|
|
|
152
153
|
for await (const event of innerStream) {
|