@sayknow-cli/ai 0.4.7 → 0.5.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/README.md +6 -0
- package/dist/types/auth-broker/client.d.ts +2 -1
- package/dist/types/auth-broker/remote-store.d.ts +2 -1
- package/dist/types/auth-broker/types.d.ts +3 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +68 -0
- package/dist/types/auth-storage.d.ts +21 -1
- package/dist/types/provider-models/openai-compat.d.ts +19 -2
- package/dist/types/providers/anthropic.d.ts +11 -1
- package/dist/types/providers/azure-openai-responses.d.ts +6 -1
- package/dist/types/providers/google-auth.d.ts +2 -0
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/dist/types/providers/google-vertex.d.ts +2 -0
- package/dist/types/providers/openai-codex-responses.d.ts +4 -0
- package/dist/types/providers/openai-completions.d.ts +2 -0
- package/dist/types/providers/openai-responses.d.ts +2 -0
- package/dist/types/providers/register-builtins.d.ts +8 -0
- package/dist/types/providers/transform-messages.d.ts +1 -0
- package/dist/types/types.d.ts +3 -1
- package/dist/types/usage/grok-cli.d.ts +3 -1
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/utils/anthropic-auth.d.ts +8 -0
- package/dist/types/utils/foundry.d.ts +10 -0
- package/dist/types/utils/http-inspector.d.ts +13 -0
- package/dist/types/utils/idle-iterator.d.ts +3 -2
- package/dist/types/utils/oauth/alibaba-token-plan.d.ts +19 -0
- package/dist/types/utils/oauth/bizrouter.d.ts +1 -0
- package/dist/types/utils/oauth/opengateway.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/auth-broker/client.ts +13 -0
- package/src/auth-broker/refresher.ts +1 -0
- package/src/auth-broker/remote-store.ts +25 -0
- package/src/auth-broker/server.ts +10 -2
- package/src/auth-broker/types.ts +4 -0
- package/src/auth-broker/wire-schemas.ts +17 -1
- package/src/auth-storage.ts +234 -45
- package/src/cli.ts +2 -0
- package/src/model-thinking.ts +11 -3
- package/src/models.json +3289 -486
- package/src/provider-models/descriptors.ts +19 -6
- package/src/provider-models/openai-compat.ts +99 -18
- package/src/providers/amazon-bedrock.ts +4 -0
- package/src/providers/anthropic.ts +131 -28
- package/src/providers/azure-openai-responses.ts +16 -3
- package/src/providers/google-auth.ts +13 -2
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/google-vertex.ts +7 -2
- package/src/providers/openai-anthropic-shim.ts +4 -0
- package/src/providers/openai-codex-responses.ts +52 -10
- package/src/providers/openai-completions-compat.ts +2 -2
- package/src/providers/openai-completions.ts +20 -3
- package/src/providers/openai-responses.ts +17 -10
- package/src/providers/register-builtins.ts +21 -2
- package/src/providers/transform-messages.ts +25 -6
- package/src/stream.ts +3 -1
- package/src/types.ts +9 -2
- package/src/usage/claude.ts +21 -3
- package/src/usage/grok-cli.ts +12 -1
- package/src/usage/kimi.ts +16 -2
- package/src/utils/anthropic-auth.ts +11 -3
- package/src/utils/foundry.ts +12 -2
- package/src/utils/http-inspector.ts +77 -0
- package/src/utils/idle-iterator.ts +20 -7
- package/src/utils/oauth/{alibaba-coding-plan.ts → alibaba-token-plan.ts} +12 -11
- package/src/utils/oauth/bizrouter.ts +15 -0
- package/src/utils/oauth/index.ts +15 -2
- package/src/utils/oauth/opengateway.ts +15 -0
- package/src/utils/oauth/types.ts +3 -1
- package/src/utils/validation.ts +17 -2
- package/src/utils.ts +41 -4
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
|
@@ -9,8 +9,9 @@ import type { OAuthProvider } from "../utils/oauth/types";
|
|
|
9
9
|
import { googleModelManagerOptions } from "./google";
|
|
10
10
|
import { ollamaCloudModelManagerOptions } from "./ollama";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
alibabaTokenPlanModelManagerOptions,
|
|
13
13
|
anthropicModelManagerOptions,
|
|
14
|
+
bizrouterModelManagerOptions,
|
|
14
15
|
cerebrasModelManagerOptions,
|
|
15
16
|
cloudflareAiGatewayModelManagerOptions,
|
|
16
17
|
deepinfraModelManagerOptions,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
openaiModelManagerOptions,
|
|
34
35
|
opencodeGoModelManagerOptions,
|
|
35
36
|
opencodeZenModelManagerOptions,
|
|
37
|
+
opengatewayModelManagerOptions,
|
|
36
38
|
openrouterModelManagerOptions,
|
|
37
39
|
qianfanModelManagerOptions,
|
|
38
40
|
qwenPortalModelManagerOptions,
|
|
@@ -130,10 +132,10 @@ function catalogDescriptor(
|
|
|
130
132
|
export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
131
133
|
descriptor("anthropic", "claude-sonnet-4-6", config => anthropicModelManagerOptions(config)),
|
|
132
134
|
catalogDescriptor(
|
|
133
|
-
"alibaba-
|
|
134
|
-
"
|
|
135
|
-
config =>
|
|
136
|
-
catalog("Alibaba
|
|
135
|
+
"alibaba-token-plan",
|
|
136
|
+
"deepseek-v4-pro",
|
|
137
|
+
config => alibabaTokenPlanModelManagerOptions(config),
|
|
138
|
+
catalog("Alibaba Token Plan", ["ALIBABA_TOKEN_PLAN_API_KEY"], { oauthProvider: "alibaba-token-plan" }),
|
|
137
139
|
),
|
|
138
140
|
descriptor("openai", "gpt-5.4", config => openaiModelManagerOptions(config)),
|
|
139
141
|
descriptor("groq", "openai/gpt-oss-120b", config => groqModelManagerOptions(config)),
|
|
@@ -312,6 +314,18 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
|
312
314
|
config => zenmuxModelManagerOptions(config),
|
|
313
315
|
catalog("ZenMux", ["ZENMUX_API_KEY"]),
|
|
314
316
|
),
|
|
317
|
+
catalogDescriptor(
|
|
318
|
+
"opengateway",
|
|
319
|
+
"openai/gpt-4o",
|
|
320
|
+
config => opengatewayModelManagerOptions(config),
|
|
321
|
+
catalog("OpenGateway by Sionic AI", ["OPENGATEWAY_API_KEY"]),
|
|
322
|
+
),
|
|
323
|
+
catalogDescriptor(
|
|
324
|
+
"bizrouter",
|
|
325
|
+
"anthropic/claude-sonnet-4.5",
|
|
326
|
+
config => bizrouterModelManagerOptions(config),
|
|
327
|
+
catalog("BizRouter", ["BIZROUTER_API_KEY"]),
|
|
328
|
+
),
|
|
315
329
|
catalogDescriptor("zai", "glm-5.2", config => zaiModelManagerOptions(config), catalog("zAI", ["ZAI_API_KEY"])),
|
|
316
330
|
catalogDescriptor(
|
|
317
331
|
"glm-zcode",
|
|
@@ -334,7 +348,6 @@ export const DEFAULT_MODEL_PER_PROVIDER: Record<KnownProvider, string> = {
|
|
|
334
348
|
...Object.fromEntries(PROVIDER_DESCRIPTORS.map(d => [d.providerId, d.defaultModel])),
|
|
335
349
|
// Providers not in PROVIDER_DESCRIPTORS (special auth or no standard discovery)
|
|
336
350
|
"azure-openai": "gpt-4.1",
|
|
337
|
-
"alibaba-coding-plan": "qwen3.5-plus",
|
|
338
351
|
"amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
|
|
339
352
|
"google-antigravity": "gemini-3-pro-high",
|
|
340
353
|
"google-gemini-cli": "gemini-2.5-pro",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $credentialEnv } from "@sayknow-cli/utils";
|
|
2
2
|
import type { ModelManagerOptions } from "../model-manager";
|
|
3
3
|
import { Effort } from "../model-thinking";
|
|
4
4
|
import { getBundledModels } from "../models";
|
|
@@ -553,13 +553,19 @@ export interface OpenAIModelManagerConfig {
|
|
|
553
553
|
baseUrl?: string;
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
+
/** Base URL for the OpenAI model manager, from trusted env only (`$env` merges the caller's `cwd/.env`). */
|
|
557
|
+
function resolveOpenAIModelManagerBaseUrl(config?: OpenAIModelManagerConfig): string {
|
|
558
|
+
return config?.baseUrl?.trim() || $credentialEnv("OPENAI_BASE_URL") || OPENAI_DEFAULT_BASE_URL;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Test seam: the model-manager base URL as resolved from trusted env. */
|
|
562
|
+
export function resolveOpenAIModelManagerBaseUrlForTest(config?: OpenAIModelManagerConfig): string {
|
|
563
|
+
return resolveOpenAIModelManagerBaseUrl(config);
|
|
564
|
+
}
|
|
565
|
+
|
|
556
566
|
export function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<"openai-responses"> {
|
|
557
567
|
const apiKey = config?.apiKey;
|
|
558
|
-
const baseUrl =
|
|
559
|
-
config?.baseUrl?.trim() ||
|
|
560
|
-
$inheritedEnv("OPENAI_BASE_URL") ||
|
|
561
|
-
$env.OPENAI_BASE_URL?.trim() ||
|
|
562
|
-
OPENAI_DEFAULT_BASE_URL;
|
|
568
|
+
const baseUrl = resolveOpenAIModelManagerBaseUrl(config);
|
|
563
569
|
const references = createBundledReferenceMap<"openai-responses">("openai");
|
|
564
570
|
return {
|
|
565
571
|
providerId: "openai",
|
|
@@ -1099,6 +1105,81 @@ export function zenmuxModelManagerOptions(config?: ZenMuxModelManagerConfig): Mo
|
|
|
1099
1105
|
};
|
|
1100
1106
|
}
|
|
1101
1107
|
|
|
1108
|
+
// ---------------------------------------------------------------------------
|
|
1109
|
+
// 10.5.1 OpenGateway by Sionic AI
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
|
|
1112
|
+
export interface OpenGatewayModelManagerConfig {
|
|
1113
|
+
apiKey?: string;
|
|
1114
|
+
baseUrl?: string;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* OpenGateway by Sionic AI — an OpenAI-compatible gateway that fronts OpenAI,
|
|
1119
|
+
* Anthropic, and Google models behind one API key. Models are discovered from
|
|
1120
|
+
* the OpenAI-compatible `/v1/models` endpoint.
|
|
1121
|
+
*/
|
|
1122
|
+
export function opengatewayModelManagerOptions(
|
|
1123
|
+
config?: OpenGatewayModelManagerConfig,
|
|
1124
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1125
|
+
return createSimpleOpenAICompletionsOptions("opengateway", "https://apis.opengateway.ai/v1", config);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// ---------------------------------------------------------------------------
|
|
1129
|
+
// 10.5.2 BizRouter
|
|
1130
|
+
// ---------------------------------------------------------------------------
|
|
1131
|
+
|
|
1132
|
+
const BIZROUTER_BASE_URL = "https://api.bizrouter.ai/v1";
|
|
1133
|
+
|
|
1134
|
+
function toBizRouterPrice(value: unknown, fallback: number): number {
|
|
1135
|
+
const parsed = toNumber(value);
|
|
1136
|
+
return parsed === undefined || parsed < 0 ? fallback : parsed;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export interface BizRouterModelManagerConfig {
|
|
1140
|
+
apiKey?: string;
|
|
1141
|
+
baseUrl?: string;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
export function bizrouterModelManagerOptions(
|
|
1145
|
+
config?: BizRouterModelManagerConfig,
|
|
1146
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1147
|
+
const apiKey = config?.apiKey;
|
|
1148
|
+
const baseUrl = config?.baseUrl ?? BIZROUTER_BASE_URL;
|
|
1149
|
+
const references = createBundledReferenceMap<"openai-completions">("bizrouter");
|
|
1150
|
+
return {
|
|
1151
|
+
providerId: "bizrouter",
|
|
1152
|
+
...(apiKey && {
|
|
1153
|
+
fetchDynamicModels: () =>
|
|
1154
|
+
fetchOpenAICompatibleModels({
|
|
1155
|
+
api: "openai-completions",
|
|
1156
|
+
provider: "bizrouter",
|
|
1157
|
+
baseUrl,
|
|
1158
|
+
apiKey,
|
|
1159
|
+
mapModel: (entry, defaults) => {
|
|
1160
|
+
const mapped = mapWithBundledReference(entry, defaults, references.get(defaults.id));
|
|
1161
|
+
return {
|
|
1162
|
+
...mapped,
|
|
1163
|
+
name: toModelName(entry.display_name, mapped.name),
|
|
1164
|
+
contextWindow: toPositiveNumber(entry.context_length, mapped.contextWindow),
|
|
1165
|
+
maxTokens: toPositiveNumber(entry.max_output_tokens, mapped.maxTokens),
|
|
1166
|
+
input: toInputCapabilities(entry.input_modalities),
|
|
1167
|
+
cost: {
|
|
1168
|
+
input: toBizRouterPrice(entry.input_price_per_1m_usd, mapped.cost.input),
|
|
1169
|
+
output: toBizRouterPrice(entry.output_price_per_1m_usd, mapped.cost.output),
|
|
1170
|
+
cacheRead: mapped.cost.cacheRead,
|
|
1171
|
+
cacheWrite: mapped.cost.cacheWrite,
|
|
1172
|
+
},
|
|
1173
|
+
api: "openai-completions",
|
|
1174
|
+
provider: "bizrouter",
|
|
1175
|
+
baseUrl,
|
|
1176
|
+
};
|
|
1177
|
+
},
|
|
1178
|
+
}),
|
|
1179
|
+
}),
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1102
1183
|
// ---------------------------------------------------------------------------
|
|
1103
1184
|
// 10.6 Kilo Gateway
|
|
1104
1185
|
// ---------------------------------------------------------------------------
|
|
@@ -1124,26 +1205,26 @@ export function kiloModelManagerOptions(config?: KiloModelManagerConfig): ModelM
|
|
|
1124
1205
|
}
|
|
1125
1206
|
|
|
1126
1207
|
// ---------------------------------------------------------------------------
|
|
1127
|
-
// Alibaba
|
|
1208
|
+
// Alibaba Token Plan
|
|
1128
1209
|
// ---------------------------------------------------------------------------
|
|
1129
1210
|
|
|
1130
|
-
export interface
|
|
1211
|
+
export interface AlibabaTokenPlanModelManagerConfig {
|
|
1131
1212
|
apiKey?: string;
|
|
1132
1213
|
baseUrl?: string;
|
|
1133
1214
|
}
|
|
1134
1215
|
|
|
1135
|
-
export function
|
|
1136
|
-
config?:
|
|
1216
|
+
export function alibabaTokenPlanModelManagerOptions(
|
|
1217
|
+
config?: AlibabaTokenPlanModelManagerConfig,
|
|
1137
1218
|
): ModelManagerOptions<"openai-completions"> {
|
|
1138
1219
|
const apiKey = config?.apiKey;
|
|
1139
|
-
const baseUrl = config?.baseUrl ?? "https://
|
|
1140
|
-
const references = createBundledReferenceMap<"openai-completions">("alibaba-
|
|
1220
|
+
const baseUrl = config?.baseUrl ?? "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
|
|
1221
|
+
const references = createBundledReferenceMap<"openai-completions">("alibaba-token-plan");
|
|
1141
1222
|
return {
|
|
1142
|
-
providerId: "alibaba-
|
|
1223
|
+
providerId: "alibaba-token-plan",
|
|
1143
1224
|
fetchDynamicModels: () =>
|
|
1144
1225
|
fetchOpenAICompatibleModels({
|
|
1145
1226
|
api: "openai-completions",
|
|
1146
|
-
provider: "alibaba-
|
|
1227
|
+
provider: "alibaba-token-plan",
|
|
1147
1228
|
baseUrl,
|
|
1148
1229
|
apiKey,
|
|
1149
1230
|
mapModel: (entry, defaults) => {
|
|
@@ -2356,11 +2437,11 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDe
|
|
|
2356
2437
|
reasoningContentField: "reasoning_content",
|
|
2357
2438
|
},
|
|
2358
2439
|
}),
|
|
2359
|
-
// --- Alibaba
|
|
2440
|
+
// --- Alibaba Token Plan ---
|
|
2360
2441
|
openAiCompletionsDescriptor(
|
|
2361
|
-
"alibaba-
|
|
2362
|
-
"alibaba-
|
|
2363
|
-
"https://
|
|
2442
|
+
"alibaba-token-plan",
|
|
2443
|
+
"alibaba-token-plan",
|
|
2444
|
+
"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
|
|
2364
2445
|
{
|
|
2365
2446
|
compat: {
|
|
2366
2447
|
supportsDeveloperRole: false,
|
|
@@ -910,11 +910,15 @@ function buildAdditionalModelRequestFields(
|
|
|
910
910
|
/**
|
|
911
911
|
* Adaptive thinking `display` is supported starting with Anthropic model Opus 4.7.
|
|
912
912
|
* Older adaptive-thinking models (Opus 4.6, Sonnet 4.6+) reject the field.
|
|
913
|
+
* Fable (5+) postdates Opus 4.7, accepts `display`, and defaults it to
|
|
914
|
+
* "omitted" — thinking tokens are billed but no content streams back — so it
|
|
915
|
+
* must opt in like Opus 4.7+ (issue #2791).
|
|
913
916
|
* Bedrock model ids are prefixed with region/inference-profile slugs (e.g.
|
|
914
917
|
* `eu.anthropic.Anthropic model-opus-4-7-...`); the regex matches the `Anthropic model-opus-X-Y`
|
|
915
918
|
* fragment regardless of prefix.
|
|
916
919
|
*/
|
|
917
920
|
function supportsAdaptiveThinkingDisplay(modelId: string): boolean {
|
|
921
|
+
if (/claude-fable-\d/.test(modelId)) return true;
|
|
918
922
|
const match = /claude-opus-(\d+)-(\d+)/.exec(modelId);
|
|
919
923
|
if (!match) return false;
|
|
920
924
|
const major = Number(match[1]);
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
RawMessageStreamEvent,
|
|
12
12
|
} from "@anthropic-ai/sdk/resources/messages";
|
|
13
13
|
import {
|
|
14
|
+
$credentialEnv,
|
|
14
15
|
$env,
|
|
15
16
|
extractHttpStatusFromError,
|
|
16
17
|
isEnoent,
|
|
@@ -60,7 +61,12 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
60
61
|
import { transportFailureFacts } from "../utils/fallback-transport";
|
|
61
62
|
import { isFoundryEnabled } from "../utils/foundry";
|
|
62
63
|
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
|
|
63
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
getProviderFirstEventTimeoutFallbackMs,
|
|
66
|
+
getStreamFirstEventTimeoutMs,
|
|
67
|
+
getStreamIdleTimeoutMs,
|
|
68
|
+
iterateWithIdleTimeout,
|
|
69
|
+
} from "../utils/idle-iterator";
|
|
64
70
|
import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse";
|
|
65
71
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
66
72
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
@@ -306,8 +312,12 @@ let warnedStopSequencesTrim = false;
|
|
|
306
312
|
/**
|
|
307
313
|
* Adaptive thinking `display` is supported starting with Anthropic model Opus 4.7.
|
|
308
314
|
* Older adaptive-thinking models (Opus 4.6, Sonnet 4.6+) reject the field.
|
|
315
|
+
* Fable (5+) postdates Opus 4.7, accepts `display`, and defaults it to
|
|
316
|
+
* "omitted" — thinking tokens are billed but no content streams back — so it
|
|
317
|
+
* must opt in like Opus 4.7+ (issue #2791).
|
|
309
318
|
*/
|
|
310
319
|
function supportsAdaptiveThinkingDisplay(modelId: string): boolean {
|
|
320
|
+
if (/claude-fable-\d/.test(modelId)) return true;
|
|
311
321
|
const match = /claude-opus-(\d+)-(\d+)/.exec(modelId);
|
|
312
322
|
if (!match) return false;
|
|
313
323
|
const major = Number(match[1]);
|
|
@@ -407,6 +417,23 @@ export function isAnthropicThinkingBlockMutationError(error: unknown): boolean {
|
|
|
407
417
|
);
|
|
408
418
|
}
|
|
409
419
|
|
|
420
|
+
/**
|
|
421
|
+
* 400 shape where a replayed `thinking`/`redacted_thinking` block fails signature
|
|
422
|
+
* validation, e.g. `messages.5.content.24: Invalid \`signature\` in \`thinking\` block`.
|
|
423
|
+
* Unlike the latest-assistant mutation error above, the cited block can sit anywhere
|
|
424
|
+
* in the replayed history, so recovery must repair every assistant message rather
|
|
425
|
+
* than only the latest one.
|
|
426
|
+
*/
|
|
427
|
+
export function isAnthropicThinkingSignatureInvalidError(error: unknown): boolean {
|
|
428
|
+
if (extractHttpStatusFromError(error) !== 400) return false;
|
|
429
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
430
|
+
return (
|
|
431
|
+
/invalid_request_error/i.test(message) &&
|
|
432
|
+
/thinking|redacted_thinking/i.test(message) &&
|
|
433
|
+
/invalid\s+`?signature`?/i.test(message)
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
410
437
|
function hasStrictAnthropicTools(params: MessageCreateParamsStreaming): boolean {
|
|
411
438
|
const tools = params.tools as Array<{ strict?: unknown }> | undefined;
|
|
412
439
|
return tools?.some(tool => tool.strict === true) ?? false;
|
|
@@ -464,7 +491,8 @@ function getCacheControl(
|
|
|
464
491
|
}
|
|
465
492
|
|
|
466
493
|
// Stealth mode: Mimic Anthropic Code headers and tool prefixing.
|
|
467
|
-
export const claudeCodeVersion = "2.1.
|
|
494
|
+
export const claudeCodeVersion = "2.1.219";
|
|
495
|
+
export const claudeCodeEntrypoint = "sdk-cli";
|
|
468
496
|
export const claudeToolPrefix: string = "proxy_";
|
|
469
497
|
export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
470
498
|
|
|
@@ -540,7 +568,7 @@ function createClaudeBillingHeader(payload: unknown): string {
|
|
|
540
568
|
const buildHash = Array.from(randomBytes, byte => byte.toString(16).padStart(2, "0"))
|
|
541
569
|
.join("")
|
|
542
570
|
.slice(0, 3);
|
|
543
|
-
return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${buildHash}; cc_entrypoint
|
|
571
|
+
return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${buildHash}; cc_entrypoint=${claudeCodeEntrypoint}; cch=${cch};`;
|
|
544
572
|
}
|
|
545
573
|
|
|
546
574
|
const CLAUDE_CLOAKING_USER_ID_REGEX =
|
|
@@ -600,6 +628,24 @@ export const stripClaudeToolPrefix = (name: string, prefixOverride: string = cla
|
|
|
600
628
|
return name.slice(prefixOverride.length);
|
|
601
629
|
};
|
|
602
630
|
|
|
631
|
+
// Anthropic requires image `data` to be standard (RFC 4648) base64: the standard
|
|
632
|
+
// alphabet only, correct quartet grouping, and padding (when present) confined to
|
|
633
|
+
// a trailing `=`/`==`. A resident image whose blob went missing bakes a
|
|
634
|
+
// human-readable placeholder into `data` (e.g. "[Session resident imageData blob
|
|
635
|
+
// missing: …]"), and other callers can pass whitespace, data URLs, or URL-safe
|
|
636
|
+
// variants — all of which the API rejects with a 400 `invalid base64 data` that
|
|
637
|
+
// fails the *entire* request and bricks the session. Validate the wire format
|
|
638
|
+
// strictly and degrade anything that is not standard base64 to text.
|
|
639
|
+
//
|
|
640
|
+
// Accepts canonical padded forms and their unpadded equivalents; rejects
|
|
641
|
+
// length % 4 === 1, misplaced/overlong padding, whitespace, data URLs, URL-safe
|
|
642
|
+
// (`-`/`_`) alphabets, prose, and empty input. The pattern has no nested
|
|
643
|
+
// quantifier, so even oversized inputs are rejected in linear time.
|
|
644
|
+
const ANTHROPIC_BASE64_IMAGE_DATA = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}(?:==)?|[A-Za-z0-9+/]{3}=?)?$/;
|
|
645
|
+
function isAnthropicBase64ImageData(data: string): boolean {
|
|
646
|
+
return data.length > 0 && data.length % 4 !== 1 && ANTHROPIC_BASE64_IMAGE_DATA.test(data);
|
|
647
|
+
}
|
|
648
|
+
|
|
603
649
|
/**
|
|
604
650
|
* Convert content blocks to Anthropic API format
|
|
605
651
|
*/
|
|
@@ -623,7 +669,18 @@ function convertContentBlocks(
|
|
|
623
669
|
.filter((block): block is TextContent => block.type === "text")
|
|
624
670
|
.map(block => block.text.toWellFormed())
|
|
625
671
|
.filter(text => text.trim().length > 0);
|
|
626
|
-
const imageBlocks
|
|
672
|
+
const imageBlocks: ImageContent[] = [];
|
|
673
|
+
for (const block of content) {
|
|
674
|
+
if (block.type !== "image") continue;
|
|
675
|
+
if (isAnthropicBase64ImageData(block.data)) {
|
|
676
|
+
imageBlocks.push(block);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
// Non-base64 image payload (e.g. a missing-blob placeholder): degrade to
|
|
680
|
+
// text so one lost image cannot invalidate the entire request.
|
|
681
|
+
const text = block.data.toWellFormed().trim();
|
|
682
|
+
if (text.length > 0) textBlocks.push(text);
|
|
683
|
+
}
|
|
627
684
|
const omittedImages = !supportsImages && imageBlocks.length > 0;
|
|
628
685
|
if (imageBlocks.length === 0 || !supportsImages) {
|
|
629
686
|
if (omittedImages) {
|
|
@@ -761,10 +818,12 @@ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: st
|
|
|
761
818
|
// calls api.z.ai directly (no zcode.z.ai gateway, no captcha). Pin the base so dynamic
|
|
762
819
|
// discovery / stale bundled catalogs / model cache can't redirect it elsewhere.
|
|
763
820
|
if (model.provider === "glm-zcode") {
|
|
764
|
-
return
|
|
821
|
+
return (
|
|
822
|
+
normalizeAnthropicBaseUrl($credentialEnv("ZCODE_PLAN_ANTHROPIC_BASE_URL")) ?? "https://api.z.ai/api/anthropic"
|
|
823
|
+
);
|
|
765
824
|
}
|
|
766
825
|
if (model.provider === "anthropic" && isFoundryEnabled()) {
|
|
767
|
-
const foundryBaseUrl = normalizeAnthropicBaseUrl($
|
|
826
|
+
const foundryBaseUrl = normalizeAnthropicBaseUrl($credentialEnv("FOUNDRY_BASE_URL"));
|
|
768
827
|
if (foundryBaseUrl) {
|
|
769
828
|
return foundryBaseUrl;
|
|
770
829
|
}
|
|
@@ -1127,6 +1186,40 @@ function shouldIgnoreAnthropicPreambleEvent(eventType: unknown): boolean {
|
|
|
1127
1186
|
return !ANTHROPIC_PRE_MESSAGE_START_EVENT_TYPES.has(eventType);
|
|
1128
1187
|
}
|
|
1129
1188
|
|
|
1189
|
+
function createAnthropicStreamProgressPredicate(): (event: unknown) => boolean {
|
|
1190
|
+
let outputTokens = -1;
|
|
1191
|
+
|
|
1192
|
+
return event => {
|
|
1193
|
+
if (!isRecord(event) || typeof event.type !== "string") return false;
|
|
1194
|
+
if (
|
|
1195
|
+
event.type === "message_start" ||
|
|
1196
|
+
event.type === "content_block_start" ||
|
|
1197
|
+
event.type === "content_block_stop" ||
|
|
1198
|
+
event.type === "message_stop"
|
|
1199
|
+
) {
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
if (event.type === "content_block_delta") {
|
|
1203
|
+
if (!isRecord(event.delta)) return false;
|
|
1204
|
+
const delta = event.delta;
|
|
1205
|
+
return (
|
|
1206
|
+
(typeof delta.text === "string" && delta.text.length > 0) ||
|
|
1207
|
+
(typeof delta.thinking === "string" && delta.thinking.length > 0) ||
|
|
1208
|
+
(typeof delta.partial_json === "string" && delta.partial_json.length > 0) ||
|
|
1209
|
+
(typeof delta.signature === "string" && delta.signature.length > 0)
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
if (event.type === "message_delta") {
|
|
1213
|
+
if (isRecord(event.delta) && event.delta.stop_reason != null) return true;
|
|
1214
|
+
if (!isRecord(event.usage) || typeof event.usage.output_tokens !== "number") return false;
|
|
1215
|
+
if (event.usage.output_tokens <= outputTokens) return false;
|
|
1216
|
+
outputTokens = event.usage.output_tokens;
|
|
1217
|
+
return true;
|
|
1218
|
+
}
|
|
1219
|
+
return false;
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1130
1223
|
function isTransientStreamEnvelopeError(error: unknown): boolean {
|
|
1131
1224
|
if (!(error instanceof Error)) return false;
|
|
1132
1225
|
return (
|
|
@@ -1283,20 +1376,19 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1283
1376
|
let strictFallbackErrorMessage: string | undefined;
|
|
1284
1377
|
let dropFastMode = providerSessionState?.fastModeDisabled ?? false;
|
|
1285
1378
|
let droppedForcedToolChoice = false;
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
)
|
|
1299
|
-
if (paramsOptions?.dropForcedToolChoice === true) {
|
|
1379
|
+
let repairLatestAssistantThinking = false;
|
|
1380
|
+
let repairAllAssistantThinking = false;
|
|
1381
|
+
const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
|
|
1382
|
+
// Degradation state is cumulative: every fallback rebuild must merge all
|
|
1383
|
+
// repairs activated so far. Rebuilding from only the immediate call lets
|
|
1384
|
+
// a later strict/forced-tool/fast-mode fallback reintroduce the rejected
|
|
1385
|
+
// shape (e.g. invalid thinking signatures or forced tool_choice), and
|
|
1386
|
+
// the one-shot thinking-repair guard then blocks recovery.
|
|
1387
|
+
let nextParams = buildParams(model, baseUrl, context, isOAuthToken, options, disableStrictTools, {
|
|
1388
|
+
repairLatestAssistantThinking,
|
|
1389
|
+
repairAllAssistantThinking,
|
|
1390
|
+
});
|
|
1391
|
+
if (droppedForcedToolChoice) {
|
|
1300
1392
|
delete nextParams.tool_choice;
|
|
1301
1393
|
}
|
|
1302
1394
|
if (disableStrictTools) {
|
|
@@ -1367,7 +1459,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1367
1459
|
firstTokenTime = undefined;
|
|
1368
1460
|
};
|
|
1369
1461
|
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs();
|
|
1370
|
-
const
|
|
1462
|
+
const firstEventFallbackMs = getProviderFirstEventTimeoutFallbackMs(model.provider);
|
|
1463
|
+
const firstEventTimeoutMs =
|
|
1464
|
+
options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs);
|
|
1371
1465
|
stream.push({ type: "start", partial: output });
|
|
1372
1466
|
// Retry loop for transient errors from the stream.
|
|
1373
1467
|
// Provider-level transport/rate-limit failures: only before any streamed content starts.
|
|
@@ -1401,6 +1495,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1401
1495
|
let sawEvent = false;
|
|
1402
1496
|
let sawMessageStart = false;
|
|
1403
1497
|
let sawTerminalEnvelope = false;
|
|
1498
|
+
const isProgressEvent = createAnthropicStreamProgressPredicate();
|
|
1404
1499
|
|
|
1405
1500
|
for await (const event of iterateWithIdleTimeout(anthropicStream, {
|
|
1406
1501
|
idleTimeoutMs,
|
|
@@ -1410,6 +1505,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1410
1505
|
onIdle: () => activeAbortTracker.abortLocally(idleTimeoutAbortError),
|
|
1411
1506
|
onFirstItemTimeout: () => activeAbortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
1412
1507
|
abortSignal: options?.signal,
|
|
1508
|
+
isProgressItem: isProgressEvent,
|
|
1413
1509
|
})) {
|
|
1414
1510
|
sawEvent = true;
|
|
1415
1511
|
if (sawProviderSafetyStop) {
|
|
@@ -1730,23 +1826,30 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1730
1826
|
registryKey: resolveToolChoice(model, options?.toolChoice).registryKey,
|
|
1731
1827
|
});
|
|
1732
1828
|
droppedForcedToolChoice = true;
|
|
1733
|
-
params = await prepareParams(
|
|
1829
|
+
params = await prepareParams();
|
|
1734
1830
|
providerRetryAttempt = 0;
|
|
1735
1831
|
resetOutputForRetry();
|
|
1736
1832
|
continue;
|
|
1737
1833
|
}
|
|
1834
|
+
const thinkingSignatureInvalid = isAnthropicThinkingSignatureInvalidError(streamFailure);
|
|
1738
1835
|
if (
|
|
1739
1836
|
!options?.fallbackManaged &&
|
|
1740
1837
|
!thinkingRepairAttempted &&
|
|
1741
1838
|
firstTokenTime === undefined &&
|
|
1742
|
-
isAnthropicThinkingBlockMutationError(streamFailure)
|
|
1839
|
+
(thinkingSignatureInvalid || isAnthropicThinkingBlockMutationError(streamFailure))
|
|
1743
1840
|
) {
|
|
1744
|
-
logger.debug("anthropic: repairing
|
|
1841
|
+
logger.debug("anthropic: repairing assistant thinking replay after provider rejection", {
|
|
1745
1842
|
model: model.id,
|
|
1843
|
+
scope: thinkingSignatureInvalid ? "all" : "latest",
|
|
1746
1844
|
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
1747
1845
|
});
|
|
1748
1846
|
thinkingRepairAttempted = true;
|
|
1749
|
-
|
|
1847
|
+
if (thinkingSignatureInvalid) {
|
|
1848
|
+
repairAllAssistantThinking = true;
|
|
1849
|
+
} else {
|
|
1850
|
+
repairLatestAssistantThinking = true;
|
|
1851
|
+
}
|
|
1852
|
+
params = await prepareParams();
|
|
1750
1853
|
providerRetryAttempt = 0;
|
|
1751
1854
|
resetOutputForRetry();
|
|
1752
1855
|
continue;
|
|
@@ -2210,13 +2313,13 @@ function buildParams(
|
|
|
2210
2313
|
isOAuthToken: boolean,
|
|
2211
2314
|
options?: AnthropicOptions,
|
|
2212
2315
|
disableStrictTools = false,
|
|
2213
|
-
repairLatestAssistantThinking
|
|
2316
|
+
thinkingRepair?: { repairLatestAssistantThinking?: boolean; repairAllAssistantThinking?: boolean },
|
|
2214
2317
|
): MessageCreateParamsStreaming {
|
|
2215
2318
|
const { mode: cacheMode, cacheControl } = getCacheControl(model, baseUrl, options?.cacheRetention);
|
|
2216
2319
|
|
|
2217
2320
|
const params: AnthropicSamplingParams = {
|
|
2218
2321
|
model: model.id,
|
|
2219
|
-
messages: convertAnthropicMessages(context.messages, model, isOAuthToken,
|
|
2322
|
+
messages: convertAnthropicMessages(context.messages, model, isOAuthToken, thinkingRepair),
|
|
2220
2323
|
max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
|
|
2221
2324
|
stream: true,
|
|
2222
2325
|
};
|
|
@@ -2407,7 +2510,7 @@ export function convertAnthropicMessages(
|
|
|
2407
2510
|
messages: Message[],
|
|
2408
2511
|
model: Model<"anthropic-messages">,
|
|
2409
2512
|
isOAuthToken: boolean,
|
|
2410
|
-
options?: { repairLatestAssistantThinking?: boolean },
|
|
2513
|
+
options?: { repairLatestAssistantThinking?: boolean; repairAllAssistantThinking?: boolean },
|
|
2411
2514
|
): MessageParam[] {
|
|
2412
2515
|
const params: MessageParam[] = [];
|
|
2413
2516
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env, extractHttpStatusFromError, logger } from "@sayknow-cli/utils";
|
|
1
|
+
import { $credentialEnv, $env, extractHttpStatusFromError, logger } from "@sayknow-cli/utils";
|
|
2
2
|
import { AzureOpenAI } from "openai";
|
|
3
3
|
import type {
|
|
4
4
|
Tool as OpenAITool,
|
|
@@ -234,8 +234,13 @@ function resolveAzureConfig(
|
|
|
234
234
|
): { baseUrl: string; apiVersion: string } {
|
|
235
235
|
const apiVersion = options?.azureApiVersion || $env.AZURE_OPENAI_API_VERSION || DEFAULT_AZURE_API_VERSION;
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
// Trusted sources only: both of these decide the request endpoint that carries
|
|
238
|
+
// the Azure credential, and `$env` merges the caller's `cwd/.env`. The resource
|
|
239
|
+
// name is the alternate constructor for the same host
|
|
240
|
+
// (`https://<resource>.openai.azure.com/openai/v1`), so it needs the same
|
|
241
|
+
// boundary as the explicit base URL.
|
|
242
|
+
const baseUrl = options?.azureBaseUrl?.trim() || $credentialEnv("AZURE_OPENAI_BASE_URL") || undefined;
|
|
243
|
+
const resourceName = options?.azureResourceName || $credentialEnv("AZURE_OPENAI_RESOURCE_NAME");
|
|
239
244
|
|
|
240
245
|
let resolvedBaseUrl = baseUrl;
|
|
241
246
|
|
|
@@ -259,6 +264,14 @@ function resolveAzureConfig(
|
|
|
259
264
|
};
|
|
260
265
|
}
|
|
261
266
|
|
|
267
|
+
/** Test seam: the Azure endpoint config as resolved from trusted env. */
|
|
268
|
+
export function resolveAzureConfigForTest(
|
|
269
|
+
model: Model<"azure-openai-responses">,
|
|
270
|
+
options?: AzureOpenAIResponsesOptions,
|
|
271
|
+
): { baseUrl: string; apiVersion: string } {
|
|
272
|
+
return resolveAzureConfig(model, options);
|
|
273
|
+
}
|
|
274
|
+
|
|
262
275
|
function createClient(model: Model<"azure-openai-responses">, apiKey: string, options?: AzureOpenAIResponsesOptions) {
|
|
263
276
|
if (!apiKey) {
|
|
264
277
|
const envKey = $env.AZURE_OPENAI_API_KEY;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { Buffer } from "node:buffer";
|
|
16
16
|
import * as os from "node:os";
|
|
17
17
|
import * as path from "node:path";
|
|
18
|
-
import { $envpos, isEnoent, logger } from "@sayknow-cli/utils";
|
|
18
|
+
import { $credentialEnv, $envpos, isEnoent, logger } from "@sayknow-cli/utils";
|
|
19
19
|
import type { FetchImpl } from "../types";
|
|
20
20
|
|
|
21
21
|
const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -70,8 +70,19 @@ async function readJsonFile<T>(filePath: string): Promise<T | undefined> {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** Test seam: the ADC credentials file path as resolved from trusted env. */
|
|
74
|
+
export function resolveAdcCredentialsPathForTest(): string | undefined {
|
|
75
|
+
return $credentialEnv("GOOGLE_APPLICATION_CREDENTIALS");
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
async function loadAdcCredentials(): Promise<{ source: string; creds: AdcFileCredentials } | undefined> {
|
|
74
|
-
|
|
79
|
+
// Trusted sources only: this path is read as service-account / authorized-user
|
|
80
|
+
// credentials and exchanged for a Google access token, so whatever can set it
|
|
81
|
+
// chooses the identity the agent authenticates as. `Bun.env` is `process.env`
|
|
82
|
+
// and the env module merges the caller's `cwd/.env` into it, so reading it
|
|
83
|
+
// there would let repository content point this at a key file it ships.
|
|
84
|
+
// `stream.ts` already resolves the same variable through `$credentialEnv`.
|
|
85
|
+
const gacPath = $credentialEnv("GOOGLE_APPLICATION_CREDENTIALS");
|
|
75
86
|
if (gacPath) {
|
|
76
87
|
const creds = await readJsonFile<AdcFileCredentials>(gacPath);
|
|
77
88
|
if (!creds) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const GEMINI_CLI_VERSION_ENV = "SKC_AI_GEMINI_CLI_VERSION";
|
|
7
7
|
export const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
|
|
8
|
-
export const DEFAULT_GEMINI_CLI_VERSION = "0.
|
|
8
|
+
export const DEFAULT_GEMINI_CLI_VERSION = "0.52.0";
|
|
9
9
|
|
|
10
10
|
export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
|
|
11
11
|
const version =
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env } from "@sayknow-cli/utils";
|
|
1
|
+
import { $credentialEnv, $env } from "@sayknow-cli/utils";
|
|
2
2
|
import type { Context, Model, StreamFunction } from "../types";
|
|
3
3
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
4
4
|
import { getVertexAccessToken } from "./google-auth";
|
|
@@ -58,12 +58,17 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
/** Test seam: the Vertex API key as resolved from options plus trusted env. */
|
|
62
|
+
export function resolveVertexApiKeyForTest(options?: GoogleVertexOptions): string | undefined {
|
|
63
|
+
return resolveApiKey(options);
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
function resolveApiKey(options?: GoogleVertexOptions): string | undefined {
|
|
62
67
|
// options.apiKey may contain sentinel values like "<authenticated>" or "N/A"
|
|
63
68
|
// leaked from the agent loop — only use it if it looks like a real API key.
|
|
64
69
|
const optKey = options?.apiKey;
|
|
65
70
|
const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
|
|
66
|
-
return realKey || $
|
|
71
|
+
return realKey || $credentialEnv("GOOGLE_CLOUD_API_KEY");
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
function resolveProject(options?: GoogleVertexOptions): string {
|
|
@@ -89,6 +89,8 @@ export function streamOpenAIAnthropicShim(
|
|
|
89
89
|
onResponse: options?.onResponse,
|
|
90
90
|
onSseEvent: options?.onSseEvent,
|
|
91
91
|
fetch: options?.fetch,
|
|
92
|
+
streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
|
|
93
|
+
streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
|
|
92
94
|
thinkingEnabled,
|
|
93
95
|
thinkingBudgetTokens: thinkingBudget,
|
|
94
96
|
});
|
|
@@ -118,6 +120,8 @@ export function streamOpenAIAnthropicShim(
|
|
|
118
120
|
onResponse: options?.onResponse,
|
|
119
121
|
onSseEvent: options?.onSseEvent,
|
|
120
122
|
fetch: options?.fetch,
|
|
123
|
+
streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
|
|
124
|
+
streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
|
|
121
125
|
reasoning: reasoningEffort,
|
|
122
126
|
});
|
|
123
127
|
|