@sayknow-cli/ai 0.3.5 → 0.3.7
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/dist/types/providers/anthropic.d.ts +1 -0
- package/dist/types/utils/oauth/deepinfra.d.ts +1 -0
- package/dist/types/utils.d.ts +1 -0
- package/package.json +2 -2
- package/src/providers/amazon-bedrock.ts +2 -2
- package/src/providers/anthropic-messages-server.ts +2 -1
- package/src/providers/anthropic.ts +107 -9
- package/src/providers/google-shared.ts +3 -3
- package/src/providers/openai-chat-server.ts +3 -2
- package/src/providers/openai-codex/request-transformer.ts +5 -0
- package/src/providers/openai-responses-server.ts +6 -3
- package/src/providers/openai-responses-shared.ts +3 -3
- package/src/rate-limit-utils.ts +9 -2
- package/src/utils/oauth/deepinfra.ts +15 -0
- package/src/utils.ts +28 -0
|
@@ -134,6 +134,7 @@ export type AnthropicClientOptionsArgs = {
|
|
|
134
134
|
onSseEvent?: AnthropicOptions["onSseEvent"];
|
|
135
135
|
fetch?: FetchImpl;
|
|
136
136
|
requestMaxRetries?: number;
|
|
137
|
+
maxRetryDelayMs?: number;
|
|
137
138
|
};
|
|
138
139
|
export type AnthropicClientOptionsResult = {
|
|
139
140
|
isOAuthToken: boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const loginDeepInfra: (options: import("./types").OAuthController) => Promise<string>;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ResponseInput } from "openai/resources/responses/responses";
|
|
|
2
2
|
import type { CacheRetention, OpenAIResponsesHistoryPayload, ProviderPayload } from "./types";
|
|
3
3
|
export { isRecord } from "@sayknow-cli/utils";
|
|
4
4
|
export declare function normalizeSystemPrompts(systemPrompt: readonly string[] | string | undefined | null): string[];
|
|
5
|
+
export declare function sanitizeJsonStrings(value: unknown): unknown;
|
|
5
6
|
export declare function toNumber(value: unknown): number | undefined;
|
|
6
7
|
export declare function toPositiveNumber(value: unknown, fallback: number): number;
|
|
7
8
|
export declare function toBoolean(value: unknown): boolean | undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/ai",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.7",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://github.com/jaybeyond/Sayknow_CLI",
|
|
7
7
|
"author": "jaybeyond",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@sayknow-cli/utils": "0.3.
|
|
46
|
+
"@sayknow-cli/utils": "0.3.7",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
|
@@ -28,7 +28,7 @@ import type {
|
|
|
28
28
|
ToolChoice,
|
|
29
29
|
ToolResultMessage,
|
|
30
30
|
} from "../types";
|
|
31
|
-
import { normalizeToolCallId, resolveCacheRetention } from "../utils";
|
|
31
|
+
import { normalizeToolCallId, resolveCacheRetention, sanitizeJsonStrings } from "../utils";
|
|
32
32
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
33
33
|
import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
34
34
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
@@ -701,7 +701,7 @@ function convertMessages(
|
|
|
701
701
|
toolUse: {
|
|
702
702
|
toolUseId: normalizeToolCallId(c.id),
|
|
703
703
|
name: c.name,
|
|
704
|
-
input: c.arguments,
|
|
704
|
+
input: sanitizeJsonStrings(c.arguments),
|
|
705
705
|
},
|
|
706
706
|
});
|
|
707
707
|
break;
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ToolResultMessage,
|
|
14
14
|
UserMessage,
|
|
15
15
|
} from "../types";
|
|
16
|
+
import { sanitizeJsonStrings } from "../utils";
|
|
16
17
|
import {
|
|
17
18
|
type AnthropicAssistantContentBlock,
|
|
18
19
|
type AnthropicMessage,
|
|
@@ -433,7 +434,7 @@ function encodeContentBlocks(message: AssistantMessage): Record<string, unknown>
|
|
|
433
434
|
blocks.push({ type: "redacted_thinking", data: c.data });
|
|
434
435
|
break;
|
|
435
436
|
case "toolCall":
|
|
436
|
-
blocks.push({ type: "tool_use", id: c.id, name: c.name, input: c.arguments ?? {} });
|
|
437
|
+
blocks.push({ type: "tool_use", id: c.id, name: c.name, input: sanitizeJsonStrings(c.arguments ?? {}) });
|
|
437
438
|
break;
|
|
438
439
|
}
|
|
439
440
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "@sayknow-cli/utils";
|
|
22
22
|
import { hasOpus47ApiRestrictions, mapEffortToAnthropicAdaptiveEffort } from "../model-thinking";
|
|
23
23
|
import { calculateCost } from "../models";
|
|
24
|
+
import { isUsageLimitError } from "../rate-limit-utils";
|
|
24
25
|
import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
|
|
25
26
|
import type {
|
|
26
27
|
Api,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
normalizeSystemPrompts,
|
|
53
54
|
normalizeToolCallId,
|
|
54
55
|
resolveCacheRetention,
|
|
56
|
+
sanitizeJsonStrings,
|
|
55
57
|
} from "../utils";
|
|
56
58
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
57
59
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
@@ -62,6 +64,7 @@ import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse";
|
|
|
62
64
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
63
65
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
64
66
|
import { isCopilotTransientModelError } from "../utils/retry";
|
|
67
|
+
import { getRetryAfterMsFromHeaders } from "../utils/retry-after";
|
|
65
68
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
66
69
|
import {
|
|
67
70
|
COMBINATOR_KEYS,
|
|
@@ -724,6 +727,7 @@ export type AnthropicClientOptionsArgs = {
|
|
|
724
727
|
onSseEvent?: AnthropicOptions["onSseEvent"];
|
|
725
728
|
fetch?: FetchImpl;
|
|
726
729
|
requestMaxRetries?: number;
|
|
730
|
+
maxRetryDelayMs?: number;
|
|
727
731
|
};
|
|
728
732
|
|
|
729
733
|
export type AnthropicClientOptionsResult = {
|
|
@@ -877,6 +881,101 @@ function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]):
|
|
|
877
881
|
return merged;
|
|
878
882
|
}
|
|
879
883
|
|
|
884
|
+
const ANTHROPIC_RETRY_DELAY_CAP_MS = 60_000;
|
|
885
|
+
const ANTHROPIC_RATE_LIMIT_HEADER_PREFIX = "anthropic-ratelimit-";
|
|
886
|
+
|
|
887
|
+
function getSafeAnthropicHeaderEvidence(headers: Headers): string[] {
|
|
888
|
+
const evidence: string[] = [];
|
|
889
|
+
for (const [name, value] of headers) {
|
|
890
|
+
const lowerName = name.toLowerCase();
|
|
891
|
+
if (
|
|
892
|
+
lowerName === "retry-after" ||
|
|
893
|
+
lowerName === "retry-after-ms" ||
|
|
894
|
+
lowerName.startsWith(ANTHROPIC_RATE_LIMIT_HEADER_PREFIX)
|
|
895
|
+
) {
|
|
896
|
+
evidence.push(`${lowerName}=${value}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return evidence.sort();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function getStringProperty(source: unknown, key: string): string | undefined {
|
|
903
|
+
if (!isRecord(source)) return undefined;
|
|
904
|
+
const value = source[key];
|
|
905
|
+
return typeof value === "string" ? value : undefined;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function appendAnthropicRateLimitEvidence(bodyText: string, headers: Headers): string {
|
|
909
|
+
const evidence = getSafeAnthropicHeaderEvidence(headers);
|
|
910
|
+
if (evidence.length === 0) return bodyText;
|
|
911
|
+
|
|
912
|
+
const suffix = ` Anthropic rate-limit evidence: ${evidence.join(", ")}`;
|
|
913
|
+
try {
|
|
914
|
+
const parsed = JSON.parse(bodyText) as unknown;
|
|
915
|
+
if (isRecord(parsed)) {
|
|
916
|
+
const error = parsed.error;
|
|
917
|
+
if (isRecord(error) && typeof error.message === "string" && !error.message.includes(suffix)) {
|
|
918
|
+
return JSON.stringify({ ...parsed, error: { ...error, message: `${error.message}${suffix}` } });
|
|
919
|
+
}
|
|
920
|
+
if (typeof parsed.message === "string" && !parsed.message.includes(suffix)) {
|
|
921
|
+
return JSON.stringify({ ...parsed, message: `${parsed.message}${suffix}` });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
} catch {}
|
|
925
|
+
|
|
926
|
+
return bodyText.includes(suffix) ? bodyText : `${bodyText}${suffix}`;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function isAnthropicUsageExhaustionResponse(
|
|
930
|
+
bodyText: string,
|
|
931
|
+
headers: Headers,
|
|
932
|
+
retryAfterMs: number | undefined,
|
|
933
|
+
retryDelayCapMs: number,
|
|
934
|
+
): boolean {
|
|
935
|
+
const overageReason = headers.get("anthropic-ratelimit-unified-overage-disabled-reason")?.toLowerCase();
|
|
936
|
+
if (overageReason === "out_of_credits") return true;
|
|
937
|
+
if (retryAfterMs !== undefined && retryAfterMs > retryDelayCapMs) return true;
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
const parsed = JSON.parse(bodyText) as unknown;
|
|
941
|
+
const error = isRecord(parsed) ? parsed.error : undefined;
|
|
942
|
+
const type = getStringProperty(error, "type") ?? getStringProperty(parsed, "type");
|
|
943
|
+
const message = getStringProperty(error, "message") ?? getStringProperty(parsed, "message") ?? bodyText;
|
|
944
|
+
return (
|
|
945
|
+
/rate_limit_error/i.test(type ?? "") &&
|
|
946
|
+
(/request would exceed your account.?s rate limit/i.test(message) || /out_of_credits/i.test(message))
|
|
947
|
+
);
|
|
948
|
+
} catch {
|
|
949
|
+
return /request would exceed your account.?s rate limit|out_of_credits/i.test(bodyText);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function wrapAnthropicFetchForBoundedRateLimits(baseFetch: FetchImpl, maxRetryDelayMs: number | undefined): FetchImpl {
|
|
954
|
+
const retryDelayCapMs = maxRetryDelayMs ?? ANTHROPIC_RETRY_DELAY_CAP_MS;
|
|
955
|
+
return Object.assign(
|
|
956
|
+
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
957
|
+
const response = await baseFetch(input, init);
|
|
958
|
+
if (response.status !== 429 || retryDelayCapMs === 0) return response;
|
|
959
|
+
|
|
960
|
+
const headers = new Headers(response.headers);
|
|
961
|
+
const retryAfterMs = getRetryAfterMsFromHeaders(headers);
|
|
962
|
+
const bodyText = await response
|
|
963
|
+
.clone()
|
|
964
|
+
.text()
|
|
965
|
+
.catch(() => "");
|
|
966
|
+
if (!isAnthropicUsageExhaustionResponse(bodyText, headers, retryAfterMs, retryDelayCapMs)) return response;
|
|
967
|
+
|
|
968
|
+
headers.set("x-should-retry", "false");
|
|
969
|
+
return new Response(appendAnthropicRateLimitEvidence(bodyText, headers), {
|
|
970
|
+
status: response.status,
|
|
971
|
+
statusText: response.statusText,
|
|
972
|
+
headers,
|
|
973
|
+
});
|
|
974
|
+
},
|
|
975
|
+
baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
880
979
|
// The Anthropic SDK logs malformed SSE frames directly before rethrowing them.
|
|
881
980
|
// We surface the resulting provider error ourselves, so keep the SDK quiet.
|
|
882
981
|
const ANTHROPIC_SDK_LOG_LEVEL = "off" as const;
|
|
@@ -1042,6 +1141,7 @@ export function isProviderRetryableError(error: unknown, provider?: string): boo
|
|
|
1042
1141
|
if (!(error instanceof Error)) return false;
|
|
1043
1142
|
if (provider === "github-copilot" && isCopilotTransientModelError(error)) return true;
|
|
1044
1143
|
const msg = error.message.toLowerCase();
|
|
1144
|
+
if (isUsageLimitError(error.message)) return false;
|
|
1045
1145
|
if (
|
|
1046
1146
|
isUnexpectedSocketCloseMessage(msg) ||
|
|
1047
1147
|
/rate.?limit|too many requests|overloaded|service.?unavailable|internal_error|stream error.*received from peer|1302|timed?\s*out while waiting for the first event|timeout waiting for first/i.test(
|
|
@@ -1166,6 +1266,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1166
1266
|
onSseEvent: options?.onSseEvent,
|
|
1167
1267
|
fetch: options?.fetch,
|
|
1168
1268
|
requestMaxRetries: options?.requestMaxRetries,
|
|
1269
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
1169
1270
|
});
|
|
1170
1271
|
client = created.client;
|
|
1171
1272
|
isOAuthToken = created.isOAuthToken;
|
|
@@ -1723,11 +1824,8 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1723
1824
|
const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
|
|
1724
1825
|
const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
|
|
1725
1826
|
const baseFetch = args.fetch ?? fetch;
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
: args.fetch
|
|
1729
|
-
? baseFetch
|
|
1730
|
-
: undefined;
|
|
1827
|
+
const boundedFetch = wrapAnthropicFetchForBoundedRateLimits(baseFetch, args.maxRetryDelayMs);
|
|
1828
|
+
const debugFetch = onSseEvent ? wrapFetchForSseDebug(boundedFetch, event => onSseEvent(event, model)) : boundedFetch;
|
|
1731
1829
|
if (model.provider === "github-copilot") {
|
|
1732
1830
|
const copilotApiKey = parseGitHubCopilotApiKey(apiKey).accessToken;
|
|
1733
1831
|
const betaFeatures = [...extraBetas];
|
|
@@ -1755,7 +1853,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1755
1853
|
dangerouslyAllowBrowser: true,
|
|
1756
1854
|
defaultHeaders,
|
|
1757
1855
|
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
1758
|
-
|
|
1856
|
+
fetch: debugFetch,
|
|
1759
1857
|
...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
|
|
1760
1858
|
};
|
|
1761
1859
|
}
|
|
@@ -1789,7 +1887,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1789
1887
|
dangerouslyAllowBrowser: true,
|
|
1790
1888
|
defaultHeaders,
|
|
1791
1889
|
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
1792
|
-
|
|
1890
|
+
fetch: debugFetch,
|
|
1793
1891
|
};
|
|
1794
1892
|
}
|
|
1795
1893
|
|
|
@@ -1802,7 +1900,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1802
1900
|
dangerouslyAllowBrowser: true,
|
|
1803
1901
|
defaultHeaders,
|
|
1804
1902
|
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
1805
|
-
|
|
1903
|
+
fetch: debugFetch,
|
|
1806
1904
|
...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
|
|
1807
1905
|
};
|
|
1808
1906
|
}
|
|
@@ -2394,7 +2492,7 @@ export function convertAnthropicMessages(
|
|
|
2394
2492
|
type: "tool_use",
|
|
2395
2493
|
id: block.id,
|
|
2396
2494
|
name: isOAuthToken ? applyClaudeToolPrefix(block.name) : block.name,
|
|
2397
|
-
input: block.arguments ?? {},
|
|
2495
|
+
input: sanitizeJsonStrings(block.arguments ?? {}),
|
|
2398
2496
|
});
|
|
2399
2497
|
}
|
|
2400
2498
|
}
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
Tool,
|
|
19
19
|
ToolCall,
|
|
20
20
|
} from "../types";
|
|
21
|
-
import { normalizeSystemPrompts } from "../utils";
|
|
21
|
+
import { normalizeSystemPrompts, sanitizeJsonStrings } from "../utils";
|
|
22
22
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
23
23
|
import { finalizeErrorMessage, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
24
24
|
import { normalizeSchemaForCCA, normalizeSchemaForGoogle, toolWireSchema } from "../utils/schema";
|
|
@@ -237,7 +237,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
237
237
|
const part: Part = {
|
|
238
238
|
functionCall: {
|
|
239
239
|
name: block.name,
|
|
240
|
-
args: block.arguments ?? {},
|
|
240
|
+
args: sanitizeJsonStrings(block.arguments ?? {}) as Record<string, unknown>,
|
|
241
241
|
...(requiresToolCallId(model.id) ? { id: block.id } : {}),
|
|
242
242
|
},
|
|
243
243
|
};
|
|
@@ -467,7 +467,7 @@ export function pushToolCallEvents(
|
|
|
467
467
|
stream.push({
|
|
468
468
|
type: "toolcall_delta",
|
|
469
469
|
contentIndex,
|
|
470
|
-
delta: JSON.stringify(toolCall.arguments),
|
|
470
|
+
delta: JSON.stringify(sanitizeJsonStrings(toolCall.arguments)),
|
|
471
471
|
partial: output,
|
|
472
472
|
});
|
|
473
473
|
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
ToolResultMessage,
|
|
20
20
|
TSchema,
|
|
21
21
|
} from "../types";
|
|
22
|
+
import { sanitizeJsonStrings } from "../utils";
|
|
22
23
|
import {
|
|
23
24
|
type OpenAIChatContentPart,
|
|
24
25
|
type OpenAIChatMessage,
|
|
@@ -453,9 +454,9 @@ function isOnlyRaw(args: Record<string, unknown>): boolean {
|
|
|
453
454
|
|
|
454
455
|
function stringifyArgs(args: Record<string, unknown>): string {
|
|
455
456
|
// `__raw` is our fallback marker for un-parseable inbound args; preserve it verbatim on the way out.
|
|
456
|
-
if (typeof args.__raw === "string" && isOnlyRaw(args)) return args.__raw;
|
|
457
|
+
if (typeof args.__raw === "string" && isOnlyRaw(args)) return args.__raw.toWellFormed();
|
|
457
458
|
try {
|
|
458
|
-
return JSON.stringify(args);
|
|
459
|
+
return JSON.stringify(sanitizeJsonStrings(args));
|
|
459
460
|
} catch {
|
|
460
461
|
return "{}";
|
|
461
462
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Effort } from "../../model-thinking";
|
|
2
2
|
import { requireSupportedEffort } from "../../model-thinking";
|
|
3
3
|
import type { Api, Model } from "../../types";
|
|
4
|
+
import { sanitizeJsonStrings } from "../../utils";
|
|
4
5
|
|
|
5
6
|
export interface ReasoningConfig {
|
|
6
7
|
effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
@@ -109,6 +110,10 @@ function normalizeInputTextPartFields(input: InputItem[] | undefined): InputItem
|
|
|
109
110
|
}
|
|
110
111
|
if (normalizedItem.type === "message") {
|
|
111
112
|
normalizedItem.content = normalizeTextPartFields(normalizedItem.content, `input[${itemIndex}].content`);
|
|
113
|
+
} else if (normalizedItem.type === "function_call" && "arguments" in itemRecord) {
|
|
114
|
+
itemRecord.arguments = sanitizeJsonStrings(itemRecord.arguments);
|
|
115
|
+
} else if (normalizedItem.type === "custom_tool_call" && typeof itemRecord.input === "string") {
|
|
116
|
+
itemRecord.input = itemRecord.input.toWellFormed();
|
|
112
117
|
}
|
|
113
118
|
return normalizedItem;
|
|
114
119
|
});
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
Tool,
|
|
23
23
|
ToolCall,
|
|
24
24
|
} from "../types";
|
|
25
|
+
import { sanitizeJsonStrings } from "../utils";
|
|
25
26
|
import {
|
|
26
27
|
type OpenAIResponsesFunctionCallItem,
|
|
27
28
|
type OpenAIResponsesFunctionCallOutputItem,
|
|
@@ -612,7 +613,8 @@ function buildOutputItems(message: AssistantMessage): OutputItem[] {
|
|
|
612
613
|
} else if (part.type === "toolCall") {
|
|
613
614
|
flushMessage();
|
|
614
615
|
if (part.customWireName) {
|
|
615
|
-
const rawInput =
|
|
616
|
+
const rawInput =
|
|
617
|
+
typeof part.arguments?.input === "string" ? (part.arguments.input as string).toWellFormed() : "";
|
|
616
618
|
out.push({
|
|
617
619
|
type: "custom_tool_call",
|
|
618
620
|
id: part.thoughtSignature ?? makeCustomCallId(),
|
|
@@ -627,7 +629,7 @@ function buildOutputItems(message: AssistantMessage): OutputItem[] {
|
|
|
627
629
|
id: part.thoughtSignature ?? makeFuncCallId(),
|
|
628
630
|
call_id: part.id,
|
|
629
631
|
name: part.name,
|
|
630
|
-
arguments: JSON.stringify(part.arguments ?? {}),
|
|
632
|
+
arguments: JSON.stringify(sanitizeJsonStrings(part.arguments ?? {})),
|
|
631
633
|
status: "completed",
|
|
632
634
|
});
|
|
633
635
|
}
|
|
@@ -1085,7 +1087,8 @@ export function encodeStream(
|
|
|
1085
1087
|
} else {
|
|
1086
1088
|
// Standard JSON tool: arguments object on the skc side, the
|
|
1087
1089
|
// wire wants the JSON string the model emitted (= streamed deltas).
|
|
1088
|
-
const argsJson =
|
|
1090
|
+
const argsJson =
|
|
1091
|
+
cur.argsText.toWellFormed() || JSON.stringify(sanitizeJsonStrings(tc.arguments ?? {}));
|
|
1089
1092
|
cur.argsText = argsJson;
|
|
1090
1093
|
emit("response.function_call_arguments.done", {
|
|
1091
1094
|
item_id: cur.itemId,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
type ToolCall,
|
|
29
29
|
type ToolResultMessage,
|
|
30
30
|
} from "../types";
|
|
31
|
-
import { normalizeResponsesToolCallId } from "../utils";
|
|
31
|
+
import { normalizeResponsesToolCallId, sanitizeJsonStrings } from "../utils";
|
|
32
32
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
33
33
|
import { isCompleteJson, parseStreamingJson } from "../utils/json-parse";
|
|
34
34
|
import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER, partitionVisionContent } from "./vision-guard";
|
|
@@ -275,7 +275,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
275
275
|
}
|
|
276
276
|
knownCallIds.add(normalized.callId);
|
|
277
277
|
if (block.customWireName) {
|
|
278
|
-
const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input : "";
|
|
278
|
+
const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input.toWellFormed() : "";
|
|
279
279
|
customCallIds?.add(normalized.callId);
|
|
280
280
|
outputItems.push({
|
|
281
281
|
type: "custom_tool_call",
|
|
@@ -291,7 +291,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
291
291
|
id: itemId,
|
|
292
292
|
call_id: normalized.callId,
|
|
293
293
|
name: block.name,
|
|
294
|
-
arguments: JSON.stringify(block.arguments),
|
|
294
|
+
arguments: JSON.stringify(sanitizeJsonStrings(block.arguments ?? {})),
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
297
|
|
package/src/rate-limit-utils.ts
CHANGED
|
@@ -36,6 +36,14 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
|
|
|
36
36
|
return "MODEL_CAPACITY_EXHAUSTED";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (
|
|
40
|
+
lower.includes("out_of_credits") ||
|
|
41
|
+
lower.includes("request would exceed your account's rate limit") ||
|
|
42
|
+
lower.includes("request would exceed your accounts rate limit")
|
|
43
|
+
) {
|
|
44
|
+
return "QUOTA_EXHAUSTED";
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
if (
|
|
40
48
|
lower.includes("per minute") ||
|
|
41
49
|
lower.includes("rate limit") ||
|
|
@@ -86,8 +94,7 @@ export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
|
|
|
86
94
|
|
|
87
95
|
/** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */
|
|
88
96
|
const USAGE_LIMIT_PATTERN =
|
|
89
|
-
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|model.?limit|model_limit_reached|message.?limit|message_limit_reached|limit for this model|quota.?exceeded|resource has been exhausted[^\n]*(?:quota|limit)/i;
|
|
90
|
-
|
|
97
|
+
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|model.?limit|model_limit_reached|message.?limit|message_limit_reached|limit for this model|quota.?exceeded|out_of_credits|request would exceed your account.?s rate limit|resource has been exhausted[^\n]*(?:quota|limit)/i;
|
|
91
98
|
export function isUsageLimitError(errorMessage: string): boolean {
|
|
92
99
|
return USAGE_LIMIT_PATTERN.test(errorMessage);
|
|
93
100
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** DeepInfra login flow (API key paste against https://api.deepinfra.com/v1/openai). */
|
|
2
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
3
|
+
|
|
4
|
+
export const loginDeepInfra = createApiKeyLogin({
|
|
5
|
+
providerLabel: "DeepInfra",
|
|
6
|
+
authUrl: "https://deepinfra.com/dash/api_keys",
|
|
7
|
+
instructions: "Create or copy your DeepInfra API key from the DeepInfra dashboard",
|
|
8
|
+
promptMessage: "Paste your DeepInfra API key",
|
|
9
|
+
placeholder: "sk-...",
|
|
10
|
+
validation: {
|
|
11
|
+
kind: "models-endpoint",
|
|
12
|
+
provider: "DeepInfra",
|
|
13
|
+
modelsUrl: "https://api.deepinfra.com/v1/openai/models",
|
|
14
|
+
},
|
|
15
|
+
});
|
package/src/utils.ts
CHANGED
|
@@ -11,6 +11,34 @@ export function normalizeSystemPrompts(systemPrompt: readonly string[] | string
|
|
|
11
11
|
return prompts.map(prompt => prompt.toWellFormed()).filter(prompt => prompt.length > 0);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function sanitizeJsonStrings(value: unknown): unknown {
|
|
15
|
+
return sanitizeJsonStringsInner(value, new WeakMap<object, unknown>());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sanitizeJsonStringsInner(value: unknown, seen: WeakMap<object, unknown>): unknown {
|
|
19
|
+
if (typeof value === "string") return value.toWellFormed();
|
|
20
|
+
if (!value || typeof value !== "object") return value;
|
|
21
|
+
|
|
22
|
+
const cached = seen.get(value);
|
|
23
|
+
if (cached !== undefined) return cached;
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
const sanitized: unknown[] = [];
|
|
27
|
+
seen.set(value, sanitized);
|
|
28
|
+
for (const item of value) {
|
|
29
|
+
sanitized.push(sanitizeJsonStringsInner(item, seen));
|
|
30
|
+
}
|
|
31
|
+
return sanitized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sanitized: Record<string, unknown> = {};
|
|
35
|
+
seen.set(value, sanitized);
|
|
36
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
37
|
+
sanitized[key.toWellFormed()] = sanitizeJsonStringsInner(nestedValue, seen);
|
|
38
|
+
}
|
|
39
|
+
return sanitized;
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
export function toNumber(value: unknown): number | undefined {
|
|
15
43
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
16
44
|
if (typeof value === "string" && value.trim()) {
|