@sayknow-cli/ai 0.5.2 → 0.5.8
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 +23 -0
- package/dist/types/model-thinking.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +10 -0
- package/dist/types/providers/anthropic.d.ts +1 -1
- package/dist/types/types.d.ts +7 -1
- package/dist/types/utils/discovery/openai-compatible.d.ts +10 -0
- package/dist/types/utils/oauth/sglang.d.ts +12 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/auth-storage.ts +32 -6
- package/src/model-thinking.ts +68 -8
- package/src/models.json +12150 -1310
- package/src/provider-models/descriptors.ts +13 -1
- package/src/provider-models/openai-compat.ts +152 -0
- package/src/providers/anthropic.ts +12 -1
- package/src/providers/openai-completions.ts +56 -14
- package/src/stream.ts +15 -21
- package/src/types.ts +9 -1
- package/src/utils/discovery/openai-compatible.ts +106 -2
- package/src/utils/oauth/index.ts +6 -0
- package/src/utils/oauth/sglang.ts +39 -0
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils/overflow.ts +64 -1
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
nanoGptModelManagerOptions,
|
|
32
32
|
nvidiaModelManagerOptions,
|
|
33
33
|
ollamaModelManagerOptions,
|
|
34
|
+
omlxModelManagerOptions,
|
|
34
35
|
openaiModelManagerOptions,
|
|
35
36
|
opencodeGoModelManagerOptions,
|
|
36
37
|
opencodeZenModelManagerOptions,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
openrouterModelManagerOptions,
|
|
39
40
|
qianfanModelManagerOptions,
|
|
40
41
|
qwenPortalModelManagerOptions,
|
|
42
|
+
sglangModelManagerOptions,
|
|
41
43
|
syntheticModelManagerOptions,
|
|
42
44
|
togetherModelManagerOptions,
|
|
43
45
|
veniceModelManagerOptions,
|
|
@@ -260,12 +262,22 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
|
260
262
|
catalog("LiteLLM", ["LITELLM_API_KEY"], { allowUnauthenticated: true }),
|
|
261
263
|
),
|
|
262
264
|
descriptor("lm-studio", "llama-3-8b", config => lmStudioModelManagerOptions(config), { allowUnauthenticated: true }),
|
|
265
|
+
descriptor("omlx", "Qwen3.5-122B-A10B-Q4", config => omlxModelManagerOptions(config), {
|
|
266
|
+
allowUnauthenticated: true,
|
|
267
|
+
}),
|
|
263
268
|
catalogDescriptor(
|
|
264
269
|
"vllm",
|
|
265
270
|
"gpt-oss-20b",
|
|
266
271
|
config => vllmModelManagerOptions(config),
|
|
267
272
|
catalog("vLLM", ["VLLM_API_KEY"], { allowUnauthenticated: true }),
|
|
268
273
|
),
|
|
274
|
+
catalogDescriptor(
|
|
275
|
+
"sglang",
|
|
276
|
+
"gpt-oss-20b",
|
|
277
|
+
config => sglangModelManagerOptions(config),
|
|
278
|
+
catalog("SGLang", ["SGLANG_API_KEY"], { allowUnauthenticated: true }),
|
|
279
|
+
{ allowUnauthenticated: true },
|
|
280
|
+
),
|
|
269
281
|
catalogDescriptor(
|
|
270
282
|
"moonshot",
|
|
271
283
|
"kimi-k2.5",
|
|
@@ -326,7 +338,7 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
|
326
338
|
config => bizrouterModelManagerOptions(config),
|
|
327
339
|
catalog("BizRouter", ["BIZROUTER_API_KEY"]),
|
|
328
340
|
),
|
|
329
|
-
catalogDescriptor("zai", "glm-5.
|
|
341
|
+
catalogDescriptor("zai", "glm-5.3", config => zaiModelManagerOptions(config), catalog("zAI", ["ZAI_API_KEY"])),
|
|
330
342
|
catalogDescriptor(
|
|
331
343
|
"glm-zcode",
|
|
332
344
|
"glm-5.2",
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
fetchOpenAICompatibleModels,
|
|
9
9
|
type OpenAICompatibleModelMapperContext,
|
|
10
10
|
type OpenAICompatibleModelRecord,
|
|
11
|
+
resolveCanonicalOpenAIBaseUrl,
|
|
12
|
+
resolveLoopbackOpenAIBaseUrl,
|
|
11
13
|
} from "../utils/discovery/openai-compatible";
|
|
12
14
|
import { toFireworksPublicModelId } from "../utils/fireworks-model-id";
|
|
13
15
|
import { getGitHubCopilotBaseUrl, OPENCODE_HEADERS, parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
@@ -197,6 +199,18 @@ function firstPositiveModelNumber(fallback: number, ...candidates: readonly unkn
|
|
|
197
199
|
return fallback;
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
function safePositiveInteger(value: unknown): number | undefined {
|
|
203
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function firstPositiveSafeInteger(fallback: number, ...candidates: readonly unknown[]): number {
|
|
207
|
+
for (const candidate of candidates) {
|
|
208
|
+
const value = safePositiveInteger(candidate);
|
|
209
|
+
if (value !== undefined) return value;
|
|
210
|
+
}
|
|
211
|
+
return fallback;
|
|
212
|
+
}
|
|
213
|
+
|
|
200
214
|
function mapLmStudioModel(
|
|
201
215
|
entry: OpenAICompatibleModelRecord,
|
|
202
216
|
defaults: Model<"openai-completions">,
|
|
@@ -226,6 +240,52 @@ function mapLmStudioModel(
|
|
|
226
240
|
),
|
|
227
241
|
};
|
|
228
242
|
}
|
|
243
|
+
function mapOmlxModel(
|
|
244
|
+
entry: OpenAICompatibleModelRecord,
|
|
245
|
+
defaults: Model<"openai-completions">,
|
|
246
|
+
reference: Model<"openai-completions"> | undefined,
|
|
247
|
+
): Model<"openai-completions"> {
|
|
248
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
249
|
+
return {
|
|
250
|
+
...model,
|
|
251
|
+
reasoning: true,
|
|
252
|
+
thinking: {
|
|
253
|
+
mode: "effort",
|
|
254
|
+
minLevel: Effort.Low,
|
|
255
|
+
maxLevel: Effort.High,
|
|
256
|
+
defaultLevel: Effort.Medium,
|
|
257
|
+
levels: [Effort.Low, Effort.Medium, Effort.High],
|
|
258
|
+
},
|
|
259
|
+
compat: {
|
|
260
|
+
...(model.compat ?? {}),
|
|
261
|
+
supportsStore: false,
|
|
262
|
+
supportsDeveloperRole: false,
|
|
263
|
+
supportsReasoningEffort: true,
|
|
264
|
+
thinkingFormat: "qwen-chat-template",
|
|
265
|
+
reasoningContentField: "reasoning_content",
|
|
266
|
+
},
|
|
267
|
+
contextWindow: firstPositiveSafeInteger(
|
|
268
|
+
model.contextWindow,
|
|
269
|
+
safePositiveInteger(entry.max_model_len),
|
|
270
|
+
entry.context_length,
|
|
271
|
+
entry.max_context_length,
|
|
272
|
+
getNestedModelValue(entry, ["meta", "n_ctx"]),
|
|
273
|
+
getNestedModelValue(entry, ["details", "context_length"]),
|
|
274
|
+
getNestedModelValue(entry, ["details", "n_ctx"]),
|
|
275
|
+
getNestedModelValue(entry, ["meta", "n_ctx_train"]),
|
|
276
|
+
),
|
|
277
|
+
maxTokens: firstPositiveSafeInteger(
|
|
278
|
+
model.maxTokens,
|
|
279
|
+
entry.max_completion_tokens,
|
|
280
|
+
entry.max_tokens,
|
|
281
|
+
entry.max_output_tokens,
|
|
282
|
+
getNestedModelValue(entry, ["details", "max_completion_tokens"]),
|
|
283
|
+
getNestedModelValue(entry, ["details", "max_tokens"]),
|
|
284
|
+
getNestedModelValue(entry, ["meta", "max_completion_tokens"]),
|
|
285
|
+
getNestedModelValue(entry, ["meta", "max_tokens"]),
|
|
286
|
+
),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
229
289
|
|
|
230
290
|
function normalizeAnthropicBaseUrl(baseUrl: string | undefined, fallback: string): string {
|
|
231
291
|
const value = baseUrl?.trim();
|
|
@@ -1381,6 +1441,42 @@ export function lmStudioModelManagerOptions(
|
|
|
1381
1441
|
}),
|
|
1382
1442
|
};
|
|
1383
1443
|
}
|
|
1444
|
+
// ---------------------------------------------------------------------------
|
|
1445
|
+
// 12.6. oMLX (Apple Silicon MLX Local Server)
|
|
1446
|
+
// ---------------------------------------------------------------------------
|
|
1447
|
+
|
|
1448
|
+
export interface OmlxModelManagerConfig {
|
|
1449
|
+
apiKey?: string;
|
|
1450
|
+
baseUrl?: string;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
export function omlxModelManagerOptions(config?: OmlxModelManagerConfig): ModelManagerOptions<"openai-completions"> {
|
|
1454
|
+
const apiKey = config?.apiKey;
|
|
1455
|
+
const baseUrl = resolveLoopbackOpenAIBaseUrl(
|
|
1456
|
+
config?.baseUrl ?? $credentialEnv("OMLX_BASE_URL"),
|
|
1457
|
+
"http://127.0.0.1:8080/v1",
|
|
1458
|
+
);
|
|
1459
|
+
const references = createBundledReferenceMap<"openai-completions">("omlx" as Parameters<typeof getBundledModels>[0]);
|
|
1460
|
+
return {
|
|
1461
|
+
providerId: "omlx",
|
|
1462
|
+
fetchDynamicModels: () =>
|
|
1463
|
+
fetchOpenAICompatibleModels({
|
|
1464
|
+
api: "openai-completions",
|
|
1465
|
+
provider: "omlx",
|
|
1466
|
+
baseUrl,
|
|
1467
|
+
apiKey,
|
|
1468
|
+
fetch: (input, init) =>
|
|
1469
|
+
fetch(input, {
|
|
1470
|
+
...init,
|
|
1471
|
+
redirect: "error",
|
|
1472
|
+
signal: init?.signal
|
|
1473
|
+
? AbortSignal.any([init.signal, AbortSignal.timeout(500)])
|
|
1474
|
+
: AbortSignal.timeout(500),
|
|
1475
|
+
}),
|
|
1476
|
+
mapModel: (entry, defaults) => mapOmlxModel(entry, defaults, references.get(defaults.id)),
|
|
1477
|
+
}),
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1384
1480
|
|
|
1385
1481
|
// ---------------------------------------------------------------------------
|
|
1386
1482
|
// 13. Synthetic
|
|
@@ -1735,6 +1831,62 @@ export function vllmModelManagerOptions(config?: VllmModelManagerConfig): ModelM
|
|
|
1735
1831
|
}),
|
|
1736
1832
|
};
|
|
1737
1833
|
}
|
|
1834
|
+
// ---------------------------------------------------------------------------
|
|
1835
|
+
// 22.5. SGLang
|
|
1836
|
+
// ---------------------------------------------------------------------------
|
|
1837
|
+
|
|
1838
|
+
export interface SglangModelManagerConfig {
|
|
1839
|
+
apiKey?: string;
|
|
1840
|
+
baseUrl?: string;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
export function sglangModelManagerOptions(
|
|
1844
|
+
config?: SglangModelManagerConfig,
|
|
1845
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1846
|
+
const apiKey = config?.apiKey;
|
|
1847
|
+
const configuredBaseUrl = config?.baseUrl ?? $credentialEnv("SGLANG_BASE_URL");
|
|
1848
|
+
const baseUrl =
|
|
1849
|
+
resolveCanonicalOpenAIBaseUrl(configuredBaseUrl) ?? (config?.baseUrl ? "" : "http://127.0.0.1:30000/v1");
|
|
1850
|
+
const isLoopback = baseUrl.length > 0 && resolveLoopbackOpenAIBaseUrl(baseUrl, "") === baseUrl;
|
|
1851
|
+
const references = createBundledReferenceMap<"openai-completions">(
|
|
1852
|
+
"sglang" as Parameters<typeof getBundledModels>[0],
|
|
1853
|
+
);
|
|
1854
|
+
return {
|
|
1855
|
+
providerId: "sglang",
|
|
1856
|
+
...(baseUrl && (apiKey || isLoopback)
|
|
1857
|
+
? {
|
|
1858
|
+
fetchDynamicModels: () =>
|
|
1859
|
+
fetchOpenAICompatibleModels({
|
|
1860
|
+
api: "openai-completions",
|
|
1861
|
+
provider: "sglang",
|
|
1862
|
+
baseUrl,
|
|
1863
|
+
apiKey,
|
|
1864
|
+
fetch: (input, init) =>
|
|
1865
|
+
fetch(input, {
|
|
1866
|
+
...init,
|
|
1867
|
+
redirect: "error",
|
|
1868
|
+
signal: isLoopback
|
|
1869
|
+
? init?.signal
|
|
1870
|
+
? AbortSignal.any([init.signal, AbortSignal.timeout(500)])
|
|
1871
|
+
: AbortSignal.timeout(500)
|
|
1872
|
+
: init?.signal,
|
|
1873
|
+
}),
|
|
1874
|
+
mapModel: (entry, defaults) => {
|
|
1875
|
+
const model = mapWithBundledReference(entry, defaults, references.get(defaults.id));
|
|
1876
|
+
const contextWindow = toNumber(entry.max_model_len);
|
|
1877
|
+
return {
|
|
1878
|
+
...model,
|
|
1879
|
+
contextWindow:
|
|
1880
|
+
contextWindow !== undefined && Number.isSafeInteger(contextWindow) && contextWindow > 0
|
|
1881
|
+
? contextWindow
|
|
1882
|
+
: model.contextWindow,
|
|
1883
|
+
};
|
|
1884
|
+
},
|
|
1885
|
+
}),
|
|
1886
|
+
}
|
|
1887
|
+
: {}),
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1738
1890
|
|
|
1739
1891
|
// ---------------------------------------------------------------------------
|
|
1740
1892
|
// 23. NanoGPT
|
|
@@ -491,7 +491,7 @@ function getCacheControl(
|
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
// Stealth mode: Mimic Anthropic Code headers and tool prefixing.
|
|
494
|
-
export const claudeCodeVersion = "2.1.
|
|
494
|
+
export const claudeCodeVersion = "2.1.267";
|
|
495
495
|
export const claudeCodeEntrypoint = "sdk-cli";
|
|
496
496
|
export const claudeToolPrefix: string = "proxy_";
|
|
497
497
|
export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
@@ -1668,6 +1668,17 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1668
1668
|
if (block) {
|
|
1669
1669
|
blocksByAnthropicIndex.delete(event.index);
|
|
1670
1670
|
delete (block as { index?: number }).index;
|
|
1671
|
+
if (block.type === "toolCall" && /\\u[0-9a-fA-F]{4}/.test(block.partialJson)) {
|
|
1672
|
+
const escapedCodeUnits = [...block.partialJson.matchAll(/\\u([0-9a-fA-F]{4})/g)].map(match =>
|
|
1673
|
+
Number.parseInt(match[1]!, 16),
|
|
1674
|
+
);
|
|
1675
|
+
if (escapedCodeUnits.some(codeUnit => codeUnit >= 0x80)) {
|
|
1676
|
+
Object.defineProperties(block, {
|
|
1677
|
+
escapedNonAsciiArguments: { value: true, configurable: true },
|
|
1678
|
+
escapedNonAsciiArgumentsRaw: { value: block.partialJson, configurable: true },
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1671
1682
|
if (block.type === "text") {
|
|
1672
1683
|
stream.push({
|
|
1673
1684
|
type: "text_end",
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { normalizeSystemPrompts, sanitizeJsonStrings } from "../utils";
|
|
39
39
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
40
40
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
41
|
+
import { transportFailureFacts } from "../utils/fallback-transport";
|
|
41
42
|
import { toFirepassWireModelId, toFireworksWireModelId } from "../utils/fireworks-model-id";
|
|
42
43
|
import {
|
|
43
44
|
type CapturedHttpErrorResponse,
|
|
@@ -465,7 +466,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
465
466
|
options?.fetch,
|
|
466
467
|
options?.streamFirstEventTimeoutMs,
|
|
467
468
|
options?.authCredentialType,
|
|
468
|
-
options?.requestMaxRetries,
|
|
469
|
+
options?.fallbackManaged ? 0 : options?.requestMaxRetries,
|
|
469
470
|
options?.sessionId,
|
|
470
471
|
);
|
|
471
472
|
const premiumRequestsTotal = copilotPremiumRequests;
|
|
@@ -500,7 +501,10 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
500
501
|
body: params,
|
|
501
502
|
};
|
|
502
503
|
const { data, response, request_id } = await client.chat.completions
|
|
503
|
-
.create(params as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming, {
|
|
504
|
+
.create(params as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming, {
|
|
505
|
+
signal: requestSignal,
|
|
506
|
+
...(options?.fallbackManaged ? { maxRetries: 0 } : {}),
|
|
507
|
+
})
|
|
504
508
|
.withResponse();
|
|
505
509
|
await notifyProviderResponse(options, response, model, request_id);
|
|
506
510
|
return data;
|
|
@@ -510,6 +514,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
510
514
|
openaiStream = await callWithCopilotModelRetry(() => createCompletionsStream(), {
|
|
511
515
|
provider: model.provider,
|
|
512
516
|
signal: requestSignal,
|
|
517
|
+
fallbackManaged: options?.fallbackManaged,
|
|
513
518
|
});
|
|
514
519
|
} catch (error) {
|
|
515
520
|
const capturedErrorResponse = getCapturedErrorResponse();
|
|
@@ -766,13 +771,25 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
766
771
|
|
|
767
772
|
if (choice.finish_reason) {
|
|
768
773
|
const finishReasonResult = mapStopReason(choice.finish_reason);
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
774
|
+
if (choice.finish_reason === "content_filter") {
|
|
775
|
+
output.errorKind = "provider_safety_stop";
|
|
776
|
+
}
|
|
777
|
+
if (output.errorKind !== "provider_safety_stop" || choice.finish_reason === "content_filter") {
|
|
778
|
+
output.stopReason = finishReasonResult.stopReason;
|
|
779
|
+
if (finishReasonResult.errorMessage) {
|
|
780
|
+
output.errorMessage = finishReasonResult.errorMessage;
|
|
781
|
+
}
|
|
772
782
|
}
|
|
773
783
|
}
|
|
774
784
|
|
|
775
785
|
if (choice.delta) {
|
|
786
|
+
const refusal = choice.delta.refusal;
|
|
787
|
+
if (typeof refusal === "string" && refusal.length > 0) {
|
|
788
|
+
output.errorKind = "provider_safety_stop";
|
|
789
|
+
output.stopReason = "error";
|
|
790
|
+
output.errorMessage = "Provider refusal";
|
|
791
|
+
appendTextDelta(refusal);
|
|
792
|
+
}
|
|
776
793
|
const normalizedDeltaText = normalizeStreamingContentText(choice.delta.content);
|
|
777
794
|
if (normalizedDeltaText.length > 0) {
|
|
778
795
|
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
@@ -944,13 +961,19 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
944
961
|
const firstEventTimeoutError = abortTracker.getLocalAbortReason();
|
|
945
962
|
output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
946
963
|
output.errorStatus = extractHttpStatusFromError(error) ?? getCapturedErrorResponse?.()?.status;
|
|
947
|
-
output.
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
964
|
+
output.transportFailure = transportFailureFacts(error);
|
|
965
|
+
if (isOpenAICompletionsSafetyStop(error, getCapturedErrorResponse?.())) {
|
|
966
|
+
output.errorKind = "provider_safety_stop";
|
|
967
|
+
}
|
|
968
|
+
if (output.errorKind !== "provider_safety_stop" || !output.errorMessage) {
|
|
969
|
+
output.errorMessage =
|
|
970
|
+
firstEventTimeoutError?.message ??
|
|
971
|
+
(await finalizeErrorMessage(error, rawRequestDump, getCapturedErrorResponse?.()));
|
|
972
|
+
// Some providers via OpenRouter include extra details here.
|
|
973
|
+
const rawMetadata = (error as { error?: { metadata?: { raw?: string } } })?.error?.metadata?.raw;
|
|
974
|
+
if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
|
|
975
|
+
output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
|
|
976
|
+
}
|
|
954
977
|
output.duration = Date.now() - startTime;
|
|
955
978
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
956
979
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
@@ -1264,8 +1287,12 @@ function buildParams(
|
|
|
1264
1287
|
// Qwen uses top-level enable_thinking: boolean
|
|
1265
1288
|
params.enable_thinking = !!options?.reasoning && !options?.disableReasoning;
|
|
1266
1289
|
} else if (supportsReasoningParams && compat.thinkingFormat === "qwen-chat-template" && model.reasoning) {
|
|
1290
|
+
const enableThinking = !!options?.reasoning && !options?.disableReasoning;
|
|
1267
1291
|
params.chat_template_kwargs = {
|
|
1268
|
-
enable_thinking:
|
|
1292
|
+
enable_thinking: enableThinking,
|
|
1293
|
+
...(enableThinking && options?.reasoning
|
|
1294
|
+
? { reasoning_effort: mapReasoningEffort(options.reasoning, compat.reasoningEffortMap) }
|
|
1295
|
+
: {}),
|
|
1269
1296
|
};
|
|
1270
1297
|
} else if (supportsReasoningParams && compat.thinkingFormat === "openrouter" && model.reasoning) {
|
|
1271
1298
|
// OpenRouter normalizes reasoning across providers via a nested reasoning object.
|
|
@@ -1926,7 +1953,22 @@ function shouldRetryWithoutStrictTools(
|
|
|
1926
1953
|
.join("\n");
|
|
1927
1954
|
return /wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool/i.test(messageParts);
|
|
1928
1955
|
}
|
|
1929
|
-
|
|
1956
|
+
function isOpenAICompletionsSafetyStop(
|
|
1957
|
+
error: unknown,
|
|
1958
|
+
capturedErrorResponse: CapturedHttpErrorResponse | undefined,
|
|
1959
|
+
): boolean {
|
|
1960
|
+
const hasContentFilterCode = (value: unknown): boolean =>
|
|
1961
|
+
typeof value === "string" && value.toLowerCase() === "content_filter";
|
|
1962
|
+
const errorRecord = error as { code?: unknown; error?: { code?: unknown } } | undefined;
|
|
1963
|
+
if (hasContentFilterCode(errorRecord?.code) || hasContentFilterCode(errorRecord?.error?.code)) return true;
|
|
1964
|
+
if (!capturedErrorResponse?.bodyText) return false;
|
|
1965
|
+
try {
|
|
1966
|
+
const body = JSON.parse(capturedErrorResponse.bodyText) as { code?: unknown; error?: { code?: unknown } };
|
|
1967
|
+
return hasContentFilterCode(body.code) || hasContentFilterCode(body.error?.code);
|
|
1968
|
+
} catch {
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1930
1972
|
function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"] | string): {
|
|
1931
1973
|
stopReason: StopReason;
|
|
1932
1974
|
errorMessage?: string;
|
package/src/stream.ts
CHANGED
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
} from "./model-thinking";
|
|
12
12
|
import type { BedrockOptions } from "./providers/amazon-bedrock";
|
|
13
13
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
14
|
+
import {
|
|
15
|
+
hasResolvableAwsProfileSource,
|
|
16
|
+
isValidBedrockBearerToken,
|
|
17
|
+
readAwsStaticEnvironmentCredentials,
|
|
18
|
+
} from "./providers/aws-credential-config";
|
|
14
19
|
import type { CursorOptions } from "./providers/cursor";
|
|
15
20
|
import { isGitLabDuoModel, streamGitLabDuo } from "./providers/gitlab-duo";
|
|
16
21
|
import type { GoogleOptions } from "./providers/google";
|
|
@@ -129,28 +134,12 @@ const serviceProviderMap: Record<string, KeyResolver> = {
|
|
|
129
134
|
return "<authenticated>";
|
|
130
135
|
}
|
|
131
136
|
},
|
|
132
|
-
// Amazon Bedrock
|
|
133
|
-
//
|
|
134
|
-
// 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY - standard IAM keys
|
|
135
|
-
// 3. AWS_BEARER_TOKEN_BEDROCK - Bedrock API keys (bearer token)
|
|
136
|
-
// 4. AWS_CONTAINER_CREDENTIALS_* - ECS/Task IAM role credentials
|
|
137
|
-
// 5. AWS_WEB_IDENTITY_TOKEN_FILE + AWS_ROLE_ARN - IRSA (EKS) web identity
|
|
137
|
+
// Amazon Bedrock is advertised only when a credential source can be
|
|
138
|
+
// resolved locally without probing network metadata providers.
|
|
138
139
|
"amazon-bedrock": () => {
|
|
139
|
-
const awsProfile = $credentialEnv("AWS_PROFILE");
|
|
140
|
-
const awsAccessKeyId = $credentialEnv("AWS_ACCESS_KEY_ID");
|
|
141
|
-
const awsSecretAccessKey = $credentialEnv("AWS_SECRET_ACCESS_KEY");
|
|
142
140
|
const awsBearerToken = $credentialEnv("AWS_BEARER_TOKEN_BEDROCK");
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
!!$credentialEnv("AWS_CONTAINER_CREDENTIALS_FULL_URI");
|
|
146
|
-
const hasWebIdentity = !!$credentialEnv("AWS_WEB_IDENTITY_TOKEN_FILE") && !!$credentialEnv("AWS_ROLE_ARN");
|
|
147
|
-
if (
|
|
148
|
-
awsProfile ||
|
|
149
|
-
(awsAccessKeyId && awsSecretAccessKey) ||
|
|
150
|
-
awsBearerToken ||
|
|
151
|
-
hasEcsCredentials ||
|
|
152
|
-
hasWebIdentity
|
|
153
|
-
) {
|
|
141
|
+
if (awsBearerToken !== undefined && !isValidBedrockBearerToken(awsBearerToken)) return undefined;
|
|
142
|
+
if (awsBearerToken || readAwsStaticEnvironmentCredentials() || hasResolvableAwsProfileSource()) {
|
|
154
143
|
return "<authenticated>";
|
|
155
144
|
}
|
|
156
145
|
},
|
|
@@ -173,6 +162,8 @@ const serviceProviderMap: Record<string, KeyResolver> = {
|
|
|
173
162
|
bizrouter: "BIZROUTER_API_KEY",
|
|
174
163
|
venice: "VENICE_API_KEY",
|
|
175
164
|
vllm: "VLLM_API_KEY",
|
|
165
|
+
omlx: "OMLX_API_KEY",
|
|
166
|
+
sglang: "SGLANG_API_KEY",
|
|
176
167
|
xiaomi: "XIAOMI_API_KEY",
|
|
177
168
|
};
|
|
178
169
|
|
|
@@ -371,7 +362,8 @@ export function streamSimple<TApi extends Api>(
|
|
|
371
362
|
context: Context,
|
|
372
363
|
options?: SimpleStreamOptions,
|
|
373
364
|
): AssistantMessageEventStream {
|
|
374
|
-
const retryApiKey =
|
|
365
|
+
const retryApiKey =
|
|
366
|
+
!options?.fallbackManaged && options?.onAuthError ? (options.apiKey ?? getEnvApiKey(model.provider)) : undefined;
|
|
375
367
|
if (retryApiKey) {
|
|
376
368
|
const outer = new AssistantMessageEventStream();
|
|
377
369
|
const onAuthError = options!.onAuthError!;
|
|
@@ -632,6 +624,8 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
632
624
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
633
625
|
requestMaxRetries: options?.requestMaxRetries,
|
|
634
626
|
streamMaxRetries: options?.streamMaxRetries,
|
|
627
|
+
fallbackManaged: options?.fallbackManaged,
|
|
628
|
+
fallbackAttempt: options?.fallbackAttempt,
|
|
635
629
|
metadata: options?.metadata,
|
|
636
630
|
sessionId: options?.sessionId,
|
|
637
631
|
providerSessionState: options?.providerSessionState,
|
package/src/types.ts
CHANGED
|
@@ -168,7 +168,9 @@ export type KnownProvider =
|
|
|
168
168
|
| "xiaomi-token-plan-ams"
|
|
169
169
|
| "xiaomi-token-plan-cn"
|
|
170
170
|
| "zenmux"
|
|
171
|
-
| "lm-studio"
|
|
171
|
+
| "lm-studio"
|
|
172
|
+
| "omlx"
|
|
173
|
+
| "sglang";
|
|
172
174
|
export type Provider = KnownProvider | string;
|
|
173
175
|
|
|
174
176
|
import type { Effort } from "./model-thinking";
|
|
@@ -529,6 +531,12 @@ export interface ToolCall {
|
|
|
529
531
|
* rejects the call with a retryable error instead.
|
|
530
532
|
*/
|
|
531
533
|
incompleteArguments?: boolean;
|
|
534
|
+
/**
|
|
535
|
+
* Transient raw JSON for a provider-detected `\uXXXX`-escaped non-ASCII
|
|
536
|
+
* tool payload. The agent loop validates and removes it before persistence.
|
|
537
|
+
*/
|
|
538
|
+
escapedNonAsciiArguments?: boolean;
|
|
539
|
+
escapedNonAsciiArgumentsRaw?: string;
|
|
532
540
|
}
|
|
533
541
|
|
|
534
542
|
export interface Usage {
|
|
@@ -1,8 +1,43 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
1
2
|
import { UNK_CONTEXT_WINDOW, UNK_MAX_TOKENS } from "@sayknow-cli/ai";
|
|
2
3
|
import * as z from "zod/v4";
|
|
3
4
|
import type { Api, FetchImpl, Model, Provider } from "../../types";
|
|
4
5
|
|
|
5
6
|
const MODELS_PATH = "/models";
|
|
7
|
+
const MAX_MODELS_RESPONSE_BYTES = 1_000_000;
|
|
8
|
+
function parseIpv6Hextets(host: string): number[] | undefined {
|
|
9
|
+
if (net.isIP(host) !== 6) return undefined;
|
|
10
|
+
const doubleColon = host.indexOf("::");
|
|
11
|
+
if (doubleColon !== host.lastIndexOf("::")) return undefined;
|
|
12
|
+
const parseSide = (value: string): number[] | undefined => {
|
|
13
|
+
if (!value) return [];
|
|
14
|
+
const parts = value.split(":");
|
|
15
|
+
if (parts.some(part => !/^[0-9a-f]{1,4}$/i.test(part))) return undefined;
|
|
16
|
+
return parts.map(part => Number.parseInt(part, 16));
|
|
17
|
+
};
|
|
18
|
+
if (doubleColon < 0) {
|
|
19
|
+
const hextets = parseSide(host);
|
|
20
|
+
return hextets?.length === 8 ? hextets : undefined;
|
|
21
|
+
}
|
|
22
|
+
const left = parseSide(host.slice(0, doubleColon));
|
|
23
|
+
const right = parseSide(host.slice(doubleColon + 2));
|
|
24
|
+
if (!left || !right) return undefined;
|
|
25
|
+
const missing = 8 - left.length - right.length;
|
|
26
|
+
if (missing < 1) return undefined;
|
|
27
|
+
return [...left, ...new Array<number>(missing).fill(0), ...right];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isLoopbackHost(hostname: string): boolean {
|
|
31
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
32
|
+
if (host === "localhost") return true;
|
|
33
|
+
if (net.isIP(host) === 4) return host.split(".", 1)[0] === "127";
|
|
34
|
+
const hextets = parseIpv6Hextets(host);
|
|
35
|
+
if (!hextets) return false;
|
|
36
|
+
const isIpv6Loopback = hextets.slice(0, 7).every(part => part === 0) && hextets[7] === 1;
|
|
37
|
+
const isIpv4MappedLoopback =
|
|
38
|
+
hextets.slice(0, 5).every(part => part === 0) && hextets[5] === 0xffff && hextets[6]! >> 8 === 0x7f;
|
|
39
|
+
return isIpv6Loopback || isIpv4MappedLoopback;
|
|
40
|
+
}
|
|
6
41
|
|
|
7
42
|
/**
|
|
8
43
|
* Minimal OpenAI-style model entry shape consumed by discovery.
|
|
@@ -128,7 +163,9 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
|
|
|
128
163
|
response = await fetchImpl(`${baseUrl}${MODELS_PATH}`, {
|
|
129
164
|
method: "GET",
|
|
130
165
|
headers: requestHeaders,
|
|
131
|
-
signal: options.signal
|
|
166
|
+
signal: options.signal
|
|
167
|
+
? AbortSignal.any([options.signal, AbortSignal.timeout(5_000)])
|
|
168
|
+
: AbortSignal.timeout(5_000),
|
|
132
169
|
});
|
|
133
170
|
} catch {
|
|
134
171
|
return null;
|
|
@@ -144,7 +181,7 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
|
|
|
144
181
|
|
|
145
182
|
let payload: unknown;
|
|
146
183
|
try {
|
|
147
|
-
payload = await response
|
|
184
|
+
payload = JSON.parse(await readModelsResponse(response));
|
|
148
185
|
} catch {
|
|
149
186
|
return null;
|
|
150
187
|
}
|
|
@@ -188,6 +225,39 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
|
|
|
188
225
|
return Array.from(deduped.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
189
226
|
}
|
|
190
227
|
|
|
228
|
+
async function readModelsResponse(response: Response): Promise<string> {
|
|
229
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
230
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_MODELS_RESPONSE_BYTES) {
|
|
231
|
+
throw new Error("OpenAI-compatible models response exceeds the size limit");
|
|
232
|
+
}
|
|
233
|
+
if (!response.body) return "";
|
|
234
|
+
const reader = response.body.getReader();
|
|
235
|
+
const chunks: Uint8Array[] = [];
|
|
236
|
+
let total = 0;
|
|
237
|
+
try {
|
|
238
|
+
while (true) {
|
|
239
|
+
const { done, value } = await reader.read();
|
|
240
|
+
if (done) break;
|
|
241
|
+
if (!value) continue;
|
|
242
|
+
total += value.byteLength;
|
|
243
|
+
if (total > MAX_MODELS_RESPONSE_BYTES) {
|
|
244
|
+
await reader.cancel();
|
|
245
|
+
throw new Error("OpenAI-compatible models response exceeds the size limit");
|
|
246
|
+
}
|
|
247
|
+
chunks.push(value);
|
|
248
|
+
}
|
|
249
|
+
} finally {
|
|
250
|
+
reader.releaseLock();
|
|
251
|
+
}
|
|
252
|
+
const body = new Uint8Array(total);
|
|
253
|
+
let offset = 0;
|
|
254
|
+
for (const chunk of chunks) {
|
|
255
|
+
body.set(chunk, offset);
|
|
256
|
+
offset += chunk.byteLength;
|
|
257
|
+
}
|
|
258
|
+
return new TextDecoder().decode(body);
|
|
259
|
+
}
|
|
260
|
+
|
|
191
261
|
function normalizeBaseUrl(baseUrl: string): string {
|
|
192
262
|
const trimmed = baseUrl.trim();
|
|
193
263
|
if (!trimmed) {
|
|
@@ -195,6 +265,40 @@ function normalizeBaseUrl(baseUrl: string): string {
|
|
|
195
265
|
}
|
|
196
266
|
return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
197
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Returns a canonical HTTP(S) OpenAI-compatible base URL without embedded URL
|
|
270
|
+
* credentials, query parameters, or fragments.
|
|
271
|
+
*/
|
|
272
|
+
export function resolveCanonicalOpenAIBaseUrl(value: string | undefined): string | undefined {
|
|
273
|
+
const candidate = value?.trim();
|
|
274
|
+
if (!candidate) return undefined;
|
|
275
|
+
try {
|
|
276
|
+
const parsed = new URL(candidate);
|
|
277
|
+
if (
|
|
278
|
+
(parsed.protocol === "http:" || parsed.protocol === "https:") &&
|
|
279
|
+
!parsed.username &&
|
|
280
|
+
!parsed.password &&
|
|
281
|
+
!parsed.search &&
|
|
282
|
+
!parsed.hash
|
|
283
|
+
) {
|
|
284
|
+
return candidate;
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// Invalid endpoint.
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Returns a local OpenAI-compatible base URL only when it is an HTTP(S)
|
|
294
|
+
* loopback endpoint; otherwise returns the trusted fallback.
|
|
295
|
+
*/
|
|
296
|
+
export function resolveLoopbackOpenAIBaseUrl(value: string | undefined, fallback: string): string {
|
|
297
|
+
const candidate = resolveCanonicalOpenAIBaseUrl(value);
|
|
298
|
+
if (!candidate) return fallback;
|
|
299
|
+
const parsed = new URL(candidate);
|
|
300
|
+
return isLoopbackHost(parsed.hostname) ? candidate : fallback;
|
|
301
|
+
}
|
|
198
302
|
|
|
199
303
|
function extractModelEntries(payload: unknown): ParsedOpenAICompatibleModelRecord[] | null {
|
|
200
304
|
return extractModelEntriesFromNode(payload);
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -255,6 +255,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
|
|
|
255
255
|
name: "vLLM (Local OpenAI-compatible)",
|
|
256
256
|
available: true,
|
|
257
257
|
},
|
|
258
|
+
{
|
|
259
|
+
id: "sglang",
|
|
260
|
+
name: "SGLang (Local OpenAI-compatible)",
|
|
261
|
+
available: true,
|
|
262
|
+
},
|
|
258
263
|
{
|
|
259
264
|
id: "cloudflare-ai-gateway",
|
|
260
265
|
name: "Cloudflare AI Gateway",
|
|
@@ -398,6 +403,7 @@ export async function refreshOAuthToken(
|
|
|
398
403
|
case "bizrouter":
|
|
399
404
|
case "opengateway":
|
|
400
405
|
case "vllm":
|
|
406
|
+
case "sglang":
|
|
401
407
|
// API keys / static bearer tokens don't expire, return as-is
|
|
402
408
|
newCredentials = credentials;
|
|
403
409
|
break;
|