@sayknow-cli/ai 0.2.2
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 +2788 -0
- package/README.md +1183 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth-broker/client.d.ts +66 -0
- package/dist/types/auth-broker/index.d.ts +5 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +96 -0
- package/dist/types/auth-broker/server.d.ts +32 -0
- package/dist/types/auth-broker/types.d.ts +105 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
- package/dist/types/auth-gateway/http.d.ts +39 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +17 -0
- package/dist/types/auth-gateway/types.d.ts +115 -0
- package/dist/types/auth-storage.d.ts +660 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/index.d.ts +51 -0
- package/dist/types/model-cache.d.ts +17 -0
- package/dist/types/model-manager.d.ts +62 -0
- package/dist/types/model-thinking.d.ts +74 -0
- package/dist/types/models.d.ts +12 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/provider-models/bundled-references.d.ts +4 -0
- package/dist/types/provider-models/descriptors.d.ts +48 -0
- package/dist/types/provider-models/google.d.ts +20 -0
- package/dist/types/provider-models/index.d.ts +5 -0
- package/dist/types/provider-models/ollama.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +244 -0
- package/dist/types/provider-models/special.d.ts +16 -0
- package/dist/types/providers/amazon-bedrock.d.ts +60 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic.d.ts +198 -0
- package/dist/types/providers/aws-credentials.d.ts +43 -0
- package/dist/types/providers/aws-eventstream.d.ts +38 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +15 -0
- package/dist/types/providers/composer-discipline.d.ts +26 -0
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
- package/dist/types/providers/cursor.d.ts +44 -0
- package/dist/types/providers/error-message.d.ts +27 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +24 -0
- package/dist/types/providers/google-gemini-cli.d.ts +72 -0
- package/dist/types/providers/google-gemini-headers.d.ts +18 -0
- package/dist/types/providers/google-shared.d.ts +173 -0
- package/dist/types/providers/google-types.d.ts +138 -0
- package/dist/types/providers/google-vertex.d.ts +7 -0
- package/dist/types/providers/google.d.ts +4 -0
- package/dist/types/providers/grammar.d.ts +1 -0
- package/dist/types/providers/kimi.d.ts +27 -0
- package/dist/types/providers/mock.d.ts +175 -0
- package/dist/types/providers/ollama.d.ts +41 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +815 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-codex/constants.d.ts +26 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
- package/dist/types/providers/openai-codex-responses.d.ts +67 -0
- package/dist/types/providers/openai-completions-compat.d.ts +27 -0
- package/dist/types/providers/openai-completions.d.ts +33 -0
- package/dist/types/providers/openai-request-transform.d.ts +4 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-shared.d.ts +89 -0
- package/dist/types/providers/openai-responses.d.ts +32 -0
- package/dist/types/providers/pi-native-client.d.ts +13 -0
- package/dist/types/providers/pi-native-server.d.ts +68 -0
- package/dist/types/providers/register-builtins.d.ts +31 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +14 -0
- package/dist/types/providers/vision-guard.d.ts +8 -0
- package/dist/types/rate-limit-utils.d.ts +19 -0
- package/dist/types/stream.d.ts +43 -0
- package/dist/types/types.d.ts +811 -0
- package/dist/types/usage/claude.d.ts +3 -0
- package/dist/types/usage/gemini.d.ts +2 -0
- package/dist/types/usage/github-copilot.d.ts +7 -0
- package/dist/types/usage/google-antigravity.d.ts +2 -0
- package/dist/types/usage/grok-cli.d.ts +10 -0
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/usage/minimax-code.d.ts +2 -0
- package/dist/types/usage/openai-codex.d.ts +3 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +258 -0
- package/dist/types/utils/abort.d.ts +19 -0
- package/dist/types/utils/anthropic-auth.d.ts +31 -0
- package/dist/types/utils/discovery/antigravity.d.ts +61 -0
- package/dist/types/utils/discovery/codex.d.ts +38 -0
- package/dist/types/utils/discovery/cursor.d.ts +23 -0
- package/dist/types/utils/discovery/gemini.d.ts +25 -0
- package/dist/types/utils/discovery/index.d.ts +4 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +74 -0
- package/dist/types/utils/event-stream.d.ts +33 -0
- package/dist/types/utils/fireworks-model-id.d.ts +10 -0
- package/dist/types/utils/foundry.d.ts +1 -0
- package/dist/types/utils/h2-fetch.d.ts +22 -0
- package/dist/types/utils/http-inspector.d.ts +35 -0
- package/dist/types/utils/idle-iterator.d.ts +67 -0
- package/dist/types/utils/json-parse.d.ts +10 -0
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
- package/dist/types/utils/oauth/anthropic.d.ts +22 -0
- package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
- package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
- package/dist/types/utils/oauth/callback-server.d.ts +60 -0
- package/dist/types/utils/oauth/cerebras.d.ts +1 -0
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/cursor.d.ts +15 -0
- package/dist/types/utils/oauth/deepseek.d.ts +10 -0
- package/dist/types/utils/oauth/firepass.d.ts +1 -0
- package/dist/types/utils/oauth/fireworks.d.ts +1 -0
- package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
- package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
- package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
- package/dist/types/utils/oauth/huggingface.d.ts +19 -0
- package/dist/types/utils/oauth/index.d.ts +38 -0
- package/dist/types/utils/oauth/kagi.d.ts +17 -0
- package/dist/types/utils/oauth/kilo.d.ts +5 -0
- package/dist/types/utils/oauth/kimi.d.ts +21 -0
- package/dist/types/utils/oauth/litellm.d.ts +18 -0
- package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
- package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
- package/dist/types/utils/oauth/moonshot.d.ts +1 -0
- package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
- package/dist/types/utils/oauth/nvidia.d.ts +18 -0
- package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
- package/dist/types/utils/oauth/ollama.d.ts +18 -0
- package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
- package/dist/types/utils/oauth/opencode.d.ts +18 -0
- package/dist/types/utils/oauth/parallel.d.ts +17 -0
- package/dist/types/utils/oauth/perplexity.d.ts +9 -0
- package/dist/types/utils/oauth/pkce.d.ts +8 -0
- package/dist/types/utils/oauth/qianfan.d.ts +17 -0
- package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
- package/dist/types/utils/oauth/synthetic.d.ts +1 -0
- package/dist/types/utils/oauth/tavily.d.ts +17 -0
- package/dist/types/utils/oauth/together.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +45 -0
- package/dist/types/utils/oauth/venice.d.ts +18 -0
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/vllm.d.ts +16 -0
- package/dist/types/utils/oauth/xai.d.ts +30 -0
- package/dist/types/utils/oauth/xiaomi.d.ts +25 -0
- package/dist/types/utils/oauth/zai.d.ts +18 -0
- package/dist/types/utils/oauth/zenmux.d.ts +1 -0
- package/dist/types/utils/overflow.d.ts +54 -0
- package/dist/types/utils/parse-bind.d.ts +23 -0
- package/dist/types/utils/provider-response.d.ts +3 -0
- package/dist/types/utils/retry-after.d.ts +3 -0
- package/dist/types/utils/retry-budget.d.ts +1 -0
- package/dist/types/utils/retry.d.ts +26 -0
- package/dist/types/utils/schema/adapt.d.ts +24 -0
- package/dist/types/utils/schema/compatibility.d.ts +30 -0
- package/dist/types/utils/schema/dereference.d.ts +11 -0
- package/dist/types/utils/schema/draft.d.ts +10 -0
- package/dist/types/utils/schema/equality.d.ts +4 -0
- package/dist/types/utils/schema/fields.d.ts +49 -0
- package/dist/types/utils/schema/index.d.ts +13 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +93 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +25 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/wire.d.ts +54 -0
- package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
- package/dist/types/utils/sse-debug.d.ts +10 -0
- package/dist/types/utils/tool-call-healing.d.ts +71 -0
- package/dist/types/utils/tool-choice-capability.d.ts +41 -0
- package/dist/types/utils/tool-choice.d.ts +50 -0
- package/dist/types/utils/validation.d.ts +17 -0
- package/dist/types/utils.d.ts +34 -0
- package/package.json +146 -0
- package/src/api-registry.ts +96 -0
- package/src/auth-broker/client.ts +358 -0
- package/src/auth-broker/index.ts +5 -0
- package/src/auth-broker/refresher.ts +127 -0
- package/src/auth-broker/remote-store.ts +623 -0
- package/src/auth-broker/server.ts +644 -0
- package/src/auth-broker/types.ts +127 -0
- package/src/auth-broker/wire-schemas.ts +200 -0
- package/src/auth-gateway/http.ts +194 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +717 -0
- package/src/auth-gateway/types.ts +134 -0
- package/src/auth-storage.ts +4179 -0
- package/src/cli.ts +263 -0
- package/src/index.ts +56 -0
- package/src/model-cache.ts +129 -0
- package/src/model-manager.ts +486 -0
- package/src/model-thinking.ts +772 -0
- package/src/models.json +75437 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +82 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +90 -0
- package/src/provider-models/bundled-references.ts +38 -0
- package/src/provider-models/descriptors.ts +327 -0
- package/src/provider-models/google.ts +91 -0
- package/src/provider-models/index.ts +5 -0
- package/src/provider-models/ollama.ts +153 -0
- package/src/provider-models/openai-compat.ts +2352 -0
- package/src/provider-models/special.ts +67 -0
- package/src/providers/amazon-bedrock.ts +937 -0
- package/src/providers/anthropic-messages-server-schema.ts +229 -0
- package/src/providers/anthropic-messages-server.ts +677 -0
- package/src/providers/anthropic.ts +2940 -0
- package/src/providers/aws-credentials.ts +501 -0
- package/src/providers/aws-eventstream.ts +185 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +379 -0
- package/src/providers/composer-discipline.ts +41 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2671 -0
- package/src/providers/error-message.ts +21 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +372 -0
- package/src/providers/google-auth.ts +252 -0
- package/src/providers/google-gemini-cli.ts +856 -0
- package/src/providers/google-gemini-headers.ts +41 -0
- package/src/providers/google-shared.ts +951 -0
- package/src/providers/google-types.ts +167 -0
- package/src/providers/google-vertex.ts +88 -0
- package/src/providers/google.ts +41 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +52 -0
- package/src/providers/mock.ts +500 -0
- package/src/providers/ollama.ts +603 -0
- package/src/providers/openai-anthropic-shim.ts +138 -0
- package/src/providers/openai-chat-server-schema.ts +243 -0
- package/src/providers/openai-chat-server.ts +635 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +161 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2774 -0
- package/src/providers/openai-completions-compat.ts +289 -0
- package/src/providers/openai-completions.ts +1935 -0
- package/src/providers/openai-request-transform.ts +136 -0
- package/src/providers/openai-responses-server-schema.ts +290 -0
- package/src/providers/openai-responses-server.ts +1190 -0
- package/src/providers/openai-responses-shared.ts +800 -0
- package/src/providers/openai-responses.ts +738 -0
- package/src/providers/pi-native-client.ts +227 -0
- package/src/providers/pi-native-server.ts +210 -0
- package/src/providers/register-builtins.ts +411 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +319 -0
- package/src/providers/vision-guard.ts +31 -0
- package/src/rate-limit-utils.ts +93 -0
- package/src/stream.ts +960 -0
- package/src/types.ts +967 -0
- package/src/usage/claude.ts +431 -0
- package/src/usage/gemini.ts +250 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +201 -0
- package/src/usage/grok-cli.ts +163 -0
- package/src/usage/kimi.ts +271 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +503 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +183 -0
- package/src/utils/abort.ts +51 -0
- package/src/utils/anthropic-auth.ts +87 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +4 -0
- package/src/utils/discovery/openai-compatible.ts +230 -0
- package/src/utils/event-stream.ts +172 -0
- package/src/utils/fireworks-model-id.ts +30 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/h2-fetch.ts +60 -0
- package/src/utils/http-inspector.ts +255 -0
- package/src/utils/idle-iterator.ts +257 -0
- package/src/utils/json-parse.ts +148 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +200 -0
- package/src/utils/oauth/api-key-login.ts +87 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +281 -0
- package/src/utils/oauth/cerebras.ts +16 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/deepseek.ts +53 -0
- package/src/utils/oauth/firepass.ts +24 -0
- package/src/utils/oauth/fireworks.ts +15 -0
- package/src/utils/oauth/github-copilot.ts +362 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +200 -0
- package/src/utils/oauth/google-gemini-cli.ts +256 -0
- package/src/utils/oauth/google-oauth-shared.ts +110 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +469 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +254 -0
- package/src/utils/oauth/litellm.ts +47 -0
- package/src/utils/oauth/lm-studio.ts +38 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +16 -0
- package/src/utils/oauth/nanogpt.ts +15 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama-cloud.ts +28 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +299 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +206 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +16 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +16 -0
- package/src/utils/oauth/types.ts +99 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xai.ts +246 -0
- package/src/utils/oauth/xiaomi.ts +199 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +15 -0
- package/src/utils/overflow.ts +137 -0
- package/src/utils/parse-bind.ts +54 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry-budget.ts +4 -0
- package/src/utils/retry.ts +54 -0
- package/src/utils/schema/CONSTRAINTS.md +164 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.ts +190 -0
- package/src/utils/schema/index.ts +13 -0
- package/src/utils/schema/json-schema-validator.ts +577 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +1588 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +97 -0
- package/src/utils/schema/types.ts +11 -0
- package/src/utils/schema/wire.ts +213 -0
- package/src/utils/schema/zod-decontaminate.ts +331 -0
- package/src/utils/sse-debug.ts +289 -0
- package/src/utils/tool-call-healing.ts +271 -0
- package/src/utils/tool-choice-capability.ts +220 -0
- package/src/utils/tool-choice.ts +99 -0
- package/src/utils/validation.ts +1019 -0
- package/src/utils.ts +178 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { APP_NAME, extractHttpStatusFromError, getLogsDir } from "@sayknow-cli/utils";
|
|
3
|
+
import { isCopilotTransientModelError } from "./retry.js";
|
|
4
|
+
import { formatErrorMessageWithRetryAfter } from "./retry-after.js";
|
|
5
|
+
|
|
6
|
+
export type RawHttpRequestDump = {
|
|
7
|
+
provider: string;
|
|
8
|
+
api: string;
|
|
9
|
+
model: string;
|
|
10
|
+
method?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
body?: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CapturedHttpErrorResponse = {
|
|
17
|
+
status: number;
|
|
18
|
+
headers?: Headers;
|
|
19
|
+
bodyText?: string;
|
|
20
|
+
bodyJson?: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ErrorWithStatus = {
|
|
24
|
+
status?: unknown;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const SENSITIVE_HEADERS = ["authorization", "x-api-key", "api-key", "cookie", "set-cookie", "proxy-authorization"];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Privacy note appended next to a saved raw HTTP request dump. The dump is
|
|
31
|
+
* sanitized (secrets/thinking redacted) but can still contain prompt content
|
|
32
|
+
* and request metadata, so we explicitly discourage pasting it into public
|
|
33
|
+
* channels (issue #438).
|
|
34
|
+
*/
|
|
35
|
+
const RAW_HTTP_REQUEST_PRIVACY_NOTE =
|
|
36
|
+
"note: this local file is for your own debugging and may contain prompt content or request metadata — review it before sharing and do not paste its contents into public channels (issues, Discord, etc.).";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Patterns that indicate the configured model is unavailable on the provider
|
|
40
|
+
* (e.g. OpenAI's "The requested model '...' does not exist."). Used to surface
|
|
41
|
+
* actionable model/provider guidance instead of only a raw 400 + log path.
|
|
42
|
+
*/
|
|
43
|
+
const MODEL_UNAVAILABLE_PATTERNS: readonly RegExp[] = [
|
|
44
|
+
/\bmodel\b[^\n]*\bdoes not exist\b/i,
|
|
45
|
+
/\bdoes not exist\b[^\n]*\bmodel\b/i,
|
|
46
|
+
/\bmodel\b[^\n]*\b(not found|unavailable|not supported|no access|does not have access)\b/i,
|
|
47
|
+
/\b(unknown|unsupported|invalid)\s+model\b/i,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/** Whether `message` (from a 400 response) signals an unavailable/unknown model. */
|
|
51
|
+
export function isModelUnavailableError(message: string, error: unknown): boolean {
|
|
52
|
+
if (extractHttpStatusFromError(error) !== 400) return false;
|
|
53
|
+
return MODEL_UNAVAILABLE_PATTERNS.some(pattern => pattern.test(message));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Actionable guidance for selecting an available model/provider. */
|
|
57
|
+
export function formatModelUnavailableGuidance(dump: RawHttpRequestDump | undefined): string {
|
|
58
|
+
const modelPart = dump?.model ? ` '${dump.model}'` : "";
|
|
59
|
+
const providerPart = dump?.provider ? ` on provider '${dump.provider}'` : "";
|
|
60
|
+
return [
|
|
61
|
+
`The configured model${modelPart}${providerPart} is not available on this account/provider.`,
|
|
62
|
+
"Pick an available model before retrying:",
|
|
63
|
+
` • List available models: ${APP_NAME} --list-models`,
|
|
64
|
+
` • Run with a model: ${APP_NAME} --model <model>`,
|
|
65
|
+
` • Configure a provider: ${APP_NAME} setup provider`,
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function appendRawHttpRequestDumpFor400(
|
|
70
|
+
message: string,
|
|
71
|
+
error: unknown,
|
|
72
|
+
dump: RawHttpRequestDump | undefined,
|
|
73
|
+
): Promise<string> {
|
|
74
|
+
if (!dump || extractHttpStatusFromError(error) !== 400) {
|
|
75
|
+
return message;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sanitizedDump = sanitizeDump(dump);
|
|
79
|
+
const fileName = `${Date.now()}-${Bun.hash(JSON.stringify(sanitizedDump)).toString(36)}.json`;
|
|
80
|
+
const filePath = path.join(getLogsDir(), "http-400-requests", fileName);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await Bun.write(filePath, `${JSON.stringify(sanitizedDump, null, 2)}\n`);
|
|
84
|
+
return `${message}\nraw-http-request=${filePath}\n${RAW_HTTP_REQUEST_PRIVACY_NOTE}`;
|
|
85
|
+
} catch (writeError) {
|
|
86
|
+
const writeMessage = writeError instanceof Error ? writeError.message : String(writeError);
|
|
87
|
+
return `${message}\nraw-http-request-save-failed=${writeMessage}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function finalizeErrorMessage(
|
|
92
|
+
error: unknown,
|
|
93
|
+
rawRequestDump: RawHttpRequestDump | undefined,
|
|
94
|
+
capturedErrorResponse?: CapturedHttpErrorResponse,
|
|
95
|
+
): Promise<string> {
|
|
96
|
+
let message = formatErrorMessageWithRetryAfter(error, capturedErrorResponse?.headers);
|
|
97
|
+
const capturedMessage = formatCapturedHttpError(capturedErrorResponse);
|
|
98
|
+
if (capturedMessage) {
|
|
99
|
+
if (/\bstatus code\s*\(no body\)/i.test(message)) {
|
|
100
|
+
message = `${capturedErrorResponse?.status ?? "HTTP"} status code: ${capturedMessage}`;
|
|
101
|
+
} else if (!message.includes(capturedMessage)) {
|
|
102
|
+
message = `${message}\n${capturedMessage}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (isModelUnavailableError(message, error)) {
|
|
106
|
+
message = `${message}\n\n${formatModelUnavailableGuidance(rawRequestDump)}`;
|
|
107
|
+
}
|
|
108
|
+
return appendRawHttpRequestDumpFor400(message, error, rawRequestDump);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function withHttpStatus(error: unknown, status: number): Error {
|
|
112
|
+
const wrapped = error instanceof Error ? error : new Error(String(error));
|
|
113
|
+
(wrapped as ErrorWithStatus).status = status;
|
|
114
|
+
return wrapped;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Rewrite error message for GitHub Copilot request failures.
|
|
119
|
+
* Must run AFTER finalizeErrorMessage since it replaces the message entirely.
|
|
120
|
+
*
|
|
121
|
+
* 400 `model_not_supported` = Copilot routing rollout gap for our OAuth client.
|
|
122
|
+
* A preview model (gpt-5.3-OpenAI code backend, gpt-5.4*, ...) flaps between 200 and
|
|
123
|
+
* 400 because only some of Copilot's backends have the model. After the
|
|
124
|
+
* in-request retry exhausts, surface guidance rather than the raw error.
|
|
125
|
+
* 401 = token invalid/expired → credential removal is safe, prompt re-login.
|
|
126
|
+
* 403 = token valid but access denied (plan, model policy, org restriction) →
|
|
127
|
+
* do NOT reuse the auth-failed string (which triggers credential removal).
|
|
128
|
+
*/
|
|
129
|
+
export function rewriteCopilotError(errorMessage: string, error: unknown, provider: string): string {
|
|
130
|
+
if (provider !== "github-copilot") return errorMessage;
|
|
131
|
+
const status = extractHttpStatusFromError(error);
|
|
132
|
+
if (status === 401) {
|
|
133
|
+
return `GitHub Copilot authentication failed (HTTP 401). Your token may have been revoked. Please re-login with /login github-copilot`;
|
|
134
|
+
}
|
|
135
|
+
if (status === 403) {
|
|
136
|
+
return `GitHub Copilot access denied (HTTP 403). Your account may not have access to this model or feature. Check your Copilot plan or model policy settings.`;
|
|
137
|
+
}
|
|
138
|
+
if (isCopilotTransientModelError(error)) {
|
|
139
|
+
return `GitHub Copilot rejected this model (HTTP 400 model_not_supported) after retries. This is a known intermittent rollout gap for preview models on OAuth clients other than VS Code. Try again in a few seconds, switch to a GA model (gpt-5-mini, gpt-5.2), or run this model from VS Code.`;
|
|
140
|
+
}
|
|
141
|
+
return errorMessage;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function sanitizeDump(dump: RawHttpRequestDump): RawHttpRequestDump {
|
|
145
|
+
return {
|
|
146
|
+
...dump,
|
|
147
|
+
headers: redactHeaders(dump.headers),
|
|
148
|
+
body: sanitizeDumpBody(dump.body),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function sanitizeDumpBody(value: unknown): unknown {
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
return value.map(item => sanitizeDumpBody(item));
|
|
155
|
+
}
|
|
156
|
+
if (!isObject(value)) {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const type = typeof value.type === "string" ? value.type : undefined;
|
|
161
|
+
const redactedKeys = getRedactedBodyKeys(type);
|
|
162
|
+
const sanitized: Record<string, unknown> = {};
|
|
163
|
+
for (const [key, property] of Object.entries(value)) {
|
|
164
|
+
if (redactedKeys.has(key)) {
|
|
165
|
+
sanitized[key] = "[redacted]";
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
sanitized[key] = sanitizeDumpBody(property);
|
|
169
|
+
}
|
|
170
|
+
return sanitized;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getRedactedBodyKeys(type: string | undefined): Set<string> {
|
|
174
|
+
const keys = new Set<string>();
|
|
175
|
+
if (type === "thinking") {
|
|
176
|
+
keys.add("thinking");
|
|
177
|
+
keys.add("signature");
|
|
178
|
+
keys.add("thinkingSignature");
|
|
179
|
+
keys.add("thoughtSignature");
|
|
180
|
+
}
|
|
181
|
+
if (type === "redacted_thinking" || type === "redactedThinking") {
|
|
182
|
+
keys.add("data");
|
|
183
|
+
}
|
|
184
|
+
return keys;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function redactHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
188
|
+
if (!headers) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const redacted: Record<string, string> = {};
|
|
193
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
194
|
+
if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
|
|
195
|
+
redacted[key] = "[redacted]";
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
redacted[key] = value;
|
|
199
|
+
}
|
|
200
|
+
return redacted;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function formatCapturedHttpError(captured: CapturedHttpErrorResponse | undefined): string | undefined {
|
|
204
|
+
if (!captured) return undefined;
|
|
205
|
+
const bodyText = captured.bodyText?.trim();
|
|
206
|
+
if (!bodyText) return undefined;
|
|
207
|
+
const payload = parseCapturedErrorPayload(captured);
|
|
208
|
+
if (!payload) return bodyText;
|
|
209
|
+
|
|
210
|
+
const errorPayload = getObjectProperty(payload, "error") ?? payload;
|
|
211
|
+
// {"error": "string"} — the error value is a plain string, not a nested object.
|
|
212
|
+
// Fall back to it when the structured fields ("message", etc.) are absent.
|
|
213
|
+
const stringError = errorPayload === payload ? getStringProperty(payload, "error") : undefined;
|
|
214
|
+
const message =
|
|
215
|
+
getStringProperty(errorPayload, "message") ?? getStringProperty(payload, "message") ?? stringError ?? bodyText;
|
|
216
|
+
const extras = [
|
|
217
|
+
getStringProperty(errorPayload, "type") ?? getStringProperty(payload, "type"),
|
|
218
|
+
getStringProperty(errorPayload, "param") ?? getStringProperty(payload, "param"),
|
|
219
|
+
getStringProperty(errorPayload, "code") ?? getStringProperty(payload, "code"),
|
|
220
|
+
]
|
|
221
|
+
.filter(Boolean)
|
|
222
|
+
.map((value, index) => {
|
|
223
|
+
if (index === 0) return `type=${value}`;
|
|
224
|
+
if (index === 1) return `param=${value}`;
|
|
225
|
+
return `code=${value}`;
|
|
226
|
+
});
|
|
227
|
+
return extras.length > 0 ? `${message} (${extras.join(" ")})` : message;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseCapturedErrorPayload(captured: CapturedHttpErrorResponse): Record<string, unknown> | undefined {
|
|
231
|
+
if (isObject(captured.bodyJson)) {
|
|
232
|
+
return captured.bodyJson;
|
|
233
|
+
}
|
|
234
|
+
if (!captured.bodyText) return undefined;
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(captured.bodyText);
|
|
237
|
+
return isObject(parsed) ? parsed : undefined;
|
|
238
|
+
} catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getObjectProperty(value: Record<string, unknown>, key: string): Record<string, unknown> | undefined {
|
|
244
|
+
const property = value[key];
|
|
245
|
+
return isObject(property) ? property : undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getStringProperty(value: Record<string, unknown>, key: string): string | undefined {
|
|
249
|
+
const property = value[key];
|
|
250
|
+
return typeof property === "string" && property.trim().length > 0 ? property : undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
254
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
255
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { $env } from "@sayknow-cli/utils";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
4
|
+
const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 100_000;
|
|
5
|
+
|
|
6
|
+
function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): number | undefined {
|
|
7
|
+
if (value === undefined) return fallback;
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
10
|
+
if (parsed <= 0) return undefined;
|
|
11
|
+
return Math.trunc(parsed);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns the idle timeout used for provider streaming transports.
|
|
16
|
+
*
|
|
17
|
+
* `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is accepted as a backward-compatible alias.
|
|
18
|
+
* Set `PI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
|
|
19
|
+
*
|
|
20
|
+
* Providers that legitimately stream much slower than the global default can pass
|
|
21
|
+
* `fallbackMs` to widen the floor used when neither env var nor caller option is set.
|
|
22
|
+
* Caller options still take precedence; env overrides still trump the fallback.
|
|
23
|
+
*/
|
|
24
|
+
export function getStreamIdleTimeoutMs(fallbackMs: number = DEFAULT_STREAM_IDLE_TIMEOUT_MS): number | undefined {
|
|
25
|
+
return normalizeIdleTimeoutMs($env.PI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS, fallbackMs);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns the idle timeout used for OpenAI-family streaming transports.
|
|
30
|
+
*
|
|
31
|
+
* Set `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
|
|
32
|
+
*/
|
|
33
|
+
export function getOpenAIStreamIdleTimeoutMs(): number | undefined {
|
|
34
|
+
return normalizeIdleTimeoutMs(
|
|
35
|
+
$env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS,
|
|
36
|
+
DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns the timeout used while waiting for the first stream event.
|
|
42
|
+
* The first token can legitimately take longer than later inter-event gaps,
|
|
43
|
+
* so the default never undershoots the steady-state idle timeout.
|
|
44
|
+
*
|
|
45
|
+
* Set `PI_STREAM_FIRST_EVENT_TIMEOUT_MS=0` to disable the watchdog.
|
|
46
|
+
*
|
|
47
|
+
* Providers whose first response can legitimately take longer (heavy reasoning,
|
|
48
|
+
* slow cold-start proxies) can pass `fallbackMs` to widen the floor used when
|
|
49
|
+
* neither env var nor caller option is set. Caller options still take precedence;
|
|
50
|
+
* env overrides still trump the fallback.
|
|
51
|
+
*/
|
|
52
|
+
export function getStreamFirstEventTimeoutMs(
|
|
53
|
+
idleTimeoutMs?: number,
|
|
54
|
+
fallbackMs: number = DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS,
|
|
55
|
+
): number | undefined {
|
|
56
|
+
const fallback = idleTimeoutMs === undefined ? fallbackMs : Math.max(fallbackMs, idleTimeoutMs);
|
|
57
|
+
return normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallback);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type Watchdog = NodeJS.Timeout | undefined;
|
|
61
|
+
|
|
62
|
+
const dummyWatchdog = setTimeout(() => {}, 1);
|
|
63
|
+
clearTimeout(dummyWatchdog);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Starts a watchdog that aborts a request if no first stream event arrives in time.
|
|
67
|
+
* Call `markFirstEventReceived()` as soon as the first event is observed.
|
|
68
|
+
*/
|
|
69
|
+
export function createWatchdog(timeoutMs: number | undefined, onTimeout: () => void): Watchdog {
|
|
70
|
+
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
71
|
+
return setTimeout(onTimeout, timeoutMs);
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface IdleTimeoutIteratorOptions {
|
|
77
|
+
watchdog?: Watchdog;
|
|
78
|
+
idleTimeoutMs?: number;
|
|
79
|
+
firstItemTimeoutMs?: number;
|
|
80
|
+
errorMessage: string;
|
|
81
|
+
firstItemErrorMessage?: string;
|
|
82
|
+
onIdle?: () => void;
|
|
83
|
+
onFirstItemTimeout?: () => void;
|
|
84
|
+
/**
|
|
85
|
+
* Optional semantic-progress predicate. Non-progress items are still yielded,
|
|
86
|
+
* but they do not reset the idle deadline. This prevents provider
|
|
87
|
+
* keepalive/no-op events from keeping a stalled tool call alive forever.
|
|
88
|
+
*/
|
|
89
|
+
isProgressItem?: (item: unknown) => boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Cancel iteration as soon as this signal aborts. Required for caller-driven
|
|
92
|
+
* cancellation (ESC) when the underlying transport does not surface signal
|
|
93
|
+
* aborts to the iterator (HTTP/2 proxies, native sockets, mocked fetch).
|
|
94
|
+
* Without this, the consumer sleeps on iterator.next() until the idle/first
|
|
95
|
+
* -event watchdog fires — observable as the issue #912 "Working… forever"
|
|
96
|
+
* symptom on the github-copilot provider.
|
|
97
|
+
*/
|
|
98
|
+
abortSignal?: AbortSignal;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Yields items from an async iterable while enforcing a maximum idle gap between items.
|
|
103
|
+
*
|
|
104
|
+
* The first item may use a shorter timeout so stuck requests can be aborted and retried
|
|
105
|
+
* before any user-visible content has streamed.
|
|
106
|
+
*/
|
|
107
|
+
export async function* iterateWithIdleTimeout<T>(
|
|
108
|
+
iterable: AsyncIterable<T>,
|
|
109
|
+
options: IdleTimeoutIteratorOptions,
|
|
110
|
+
): AsyncGenerator<T> {
|
|
111
|
+
let watchdog = options.watchdog;
|
|
112
|
+
const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs;
|
|
113
|
+
const abortSignal = options.abortSignal;
|
|
114
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
115
|
+
|
|
116
|
+
const closeIterator = (): void => {
|
|
117
|
+
const returnPromise = iterator.return?.();
|
|
118
|
+
if (returnPromise) {
|
|
119
|
+
void returnPromise.catch(() => {});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (abortSignal?.aborted) {
|
|
124
|
+
closeIterator();
|
|
125
|
+
throw abortReason(abortSignal);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const withRacy = <T>(promise: Promise<T>) =>
|
|
129
|
+
promise.then(
|
|
130
|
+
result => ({ kind: "next" as const, result }),
|
|
131
|
+
error => ({ kind: "error" as const, error }),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
let awaitingFirstItem = true;
|
|
135
|
+
const markFirstItemReceived = () => {
|
|
136
|
+
watchdog && clearTimeout(watchdog);
|
|
137
|
+
watchdog = undefined;
|
|
138
|
+
awaitingFirstItem = false;
|
|
139
|
+
};
|
|
140
|
+
const isProgressItem = (item: T): boolean => {
|
|
141
|
+
if (!options.isProgressItem) return true;
|
|
142
|
+
try {
|
|
143
|
+
return options.isProgressItem(item);
|
|
144
|
+
} catch {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
let lastProgressAt = Date.now();
|
|
149
|
+
|
|
150
|
+
const noTimeoutEnforced =
|
|
151
|
+
(firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) &&
|
|
152
|
+
(options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0);
|
|
153
|
+
|
|
154
|
+
while (true) {
|
|
155
|
+
let activeTimeoutMs: number | undefined;
|
|
156
|
+
if (awaitingFirstItem) {
|
|
157
|
+
activeTimeoutMs = firstItemTimeoutMs;
|
|
158
|
+
} else if (options.idleTimeoutMs !== undefined && options.idleTimeoutMs > 0) {
|
|
159
|
+
activeTimeoutMs = options.idleTimeoutMs - (Date.now() - lastProgressAt);
|
|
160
|
+
// The idle deadline may already have elapsed because the *consumer*
|
|
161
|
+
// was slow, not because the provider stalled — and the next item may
|
|
162
|
+
// already be buffered and ready to deliver. Clamp to 0 instead of
|
|
163
|
+
// throwing eagerly so the next() race below still gets a chance to
|
|
164
|
+
// win (it settles on a microtask, ahead of the 0ms timer). Only a
|
|
165
|
+
// genuinely hung iterator loses that race and surfaces as a stall.
|
|
166
|
+
if (activeTimeoutMs < 0) {
|
|
167
|
+
activeTimeoutMs = 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const nextResultPromise = withRacy(iterator.next());
|
|
172
|
+
|
|
173
|
+
const racers: Array<
|
|
174
|
+
Promise<
|
|
175
|
+
| { kind: "next"; result: IteratorResult<T> }
|
|
176
|
+
| { kind: "error"; error: unknown }
|
|
177
|
+
| { kind: "timeout" }
|
|
178
|
+
| { kind: "abort" }
|
|
179
|
+
>
|
|
180
|
+
> = [nextResultPromise];
|
|
181
|
+
|
|
182
|
+
let timer: NodeJS.Timeout | undefined;
|
|
183
|
+
let resolveTimeout: ((value: { kind: "timeout" }) => void) | undefined;
|
|
184
|
+
const enforceTimeout =
|
|
185
|
+
!noTimeoutEnforced &&
|
|
186
|
+
activeTimeoutMs !== undefined &&
|
|
187
|
+
(awaitingFirstItem ? activeTimeoutMs > 0 : activeTimeoutMs >= 0);
|
|
188
|
+
if (enforceTimeout) {
|
|
189
|
+
const { promise, resolve } = Promise.withResolvers<{ kind: "timeout" }>();
|
|
190
|
+
resolveTimeout = resolve;
|
|
191
|
+
timer = setTimeout(() => resolve({ kind: "timeout" }), activeTimeoutMs);
|
|
192
|
+
racers.push(promise);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let abortListener: (() => void) | undefined;
|
|
196
|
+
let resolveAbort: ((value: { kind: "abort" }) => void) | undefined;
|
|
197
|
+
if (abortSignal) {
|
|
198
|
+
const { promise, resolve } = Promise.withResolvers<{ kind: "abort" }>();
|
|
199
|
+
resolveAbort = resolve;
|
|
200
|
+
abortListener = () => resolve({ kind: "abort" });
|
|
201
|
+
abortSignal.addEventListener("abort", abortListener, { once: true });
|
|
202
|
+
racers.push(promise);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const outcome = await Promise.race(racers);
|
|
207
|
+
if (outcome.kind === "abort") {
|
|
208
|
+
closeIterator();
|
|
209
|
+
throw abortReason(abortSignal!);
|
|
210
|
+
}
|
|
211
|
+
if (outcome.kind === "timeout") {
|
|
212
|
+
if (!awaitingFirstItem) {
|
|
213
|
+
options.onIdle?.();
|
|
214
|
+
} else {
|
|
215
|
+
options.onFirstItemTimeout?.();
|
|
216
|
+
}
|
|
217
|
+
closeIterator();
|
|
218
|
+
throw new Error(
|
|
219
|
+
!awaitingFirstItem ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (outcome.kind === "error") {
|
|
223
|
+
throw outcome.error;
|
|
224
|
+
}
|
|
225
|
+
if (outcome.result.done) {
|
|
226
|
+
markFirstItemReceived();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const item = outcome.result.value;
|
|
230
|
+
// Non-progress items (e.g. provider keepalives, synthetic `start` events that
|
|
231
|
+
// arrive before the model has produced any tokens) MUST NOT flip us out of
|
|
232
|
+
// `awaitingFirstItem`. Otherwise the next iteration switches from the (longer)
|
|
233
|
+
// first-item watchdog to the (shorter) idle watchdog while we're still waiting
|
|
234
|
+
// on the model's first real output.
|
|
235
|
+
if (isProgressItem(item)) {
|
|
236
|
+
markFirstItemReceived();
|
|
237
|
+
lastProgressAt = Date.now();
|
|
238
|
+
}
|
|
239
|
+
yield item;
|
|
240
|
+
} finally {
|
|
241
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
242
|
+
// Resolve dangling promises so the racers don't leak (Promise.race is one-shot).
|
|
243
|
+
resolveTimeout?.({ kind: "timeout" });
|
|
244
|
+
if (abortListener && abortSignal) {
|
|
245
|
+
abortSignal.removeEventListener("abort", abortListener);
|
|
246
|
+
}
|
|
247
|
+
resolveAbort?.({ kind: "abort" });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function abortReason(signal: AbortSignal): Error {
|
|
253
|
+
const reason = signal.reason;
|
|
254
|
+
if (reason instanceof Error) return reason;
|
|
255
|
+
if (typeof reason === "string") return new Error(reason);
|
|
256
|
+
return new Error("Request was aborted");
|
|
257
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { parse as partialParse } from "partial-json";
|
|
2
|
+
|
|
3
|
+
const QUOTE = 0x22;
|
|
4
|
+
const BACKSLASH = 0x5c;
|
|
5
|
+
const U = 0x75;
|
|
6
|
+
|
|
7
|
+
// Valid chars after `\`: " \ / b f n r t u
|
|
8
|
+
const VALID_ESCAPE_CHAR = new Uint8Array(128);
|
|
9
|
+
for (const ch of '"\\/bfnrtu') VALID_ESCAPE_CHAR[ch.charCodeAt(0)] = 1;
|
|
10
|
+
|
|
11
|
+
const CONTROL_ESCAPES: readonly string[] = (() => {
|
|
12
|
+
const e: string[] = [];
|
|
13
|
+
e[0x08] = "\\b";
|
|
14
|
+
e[0x09] = "\\t";
|
|
15
|
+
e[0x0a] = "\\n";
|
|
16
|
+
e[0x0c] = "\\f";
|
|
17
|
+
e[0x0d] = "\\r";
|
|
18
|
+
for (let cp = 0; cp <= 0x1f; cp++) {
|
|
19
|
+
e[cp] ??= `\\u${cp.toString(16).padStart(4, "0")}`;
|
|
20
|
+
}
|
|
21
|
+
return e;
|
|
22
|
+
})();
|
|
23
|
+
|
|
24
|
+
function isHexDigit(cp: number): boolean {
|
|
25
|
+
return (cp >= 0x30 && cp <= 0x39) || ((cp | 0x20) >= 0x61 && (cp | 0x20) <= 0x66);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function repairJson(json: string): string {
|
|
29
|
+
const len = json.length;
|
|
30
|
+
const parts: string[] = [];
|
|
31
|
+
let lastEmit = 0;
|
|
32
|
+
let inString = false;
|
|
33
|
+
let i = 0;
|
|
34
|
+
|
|
35
|
+
while (i < len) {
|
|
36
|
+
if (!inString) {
|
|
37
|
+
// Fast scan: skip to next quote.
|
|
38
|
+
while (i < len && json.charCodeAt(i) !== QUOTE) i++;
|
|
39
|
+
if (i >= len) break;
|
|
40
|
+
inString = true;
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fast scan inside string: advance past chars that need no handling.
|
|
46
|
+
while (i < len) {
|
|
47
|
+
const cp = json.charCodeAt(i);
|
|
48
|
+
if (cp < 0x20 || cp === QUOTE || cp === BACKSLASH) break;
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
if (i >= len) break;
|
|
52
|
+
|
|
53
|
+
const cp = json.charCodeAt(i);
|
|
54
|
+
|
|
55
|
+
if (cp === QUOTE) {
|
|
56
|
+
inString = false;
|
|
57
|
+
i++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (cp === BACKSLASH) {
|
|
62
|
+
// Need at least one char after the backslash; treat EOI as invalid escape.
|
|
63
|
+
if (i + 1 >= len) {
|
|
64
|
+
parts.push(json.slice(lastEmit, i), "\\\\");
|
|
65
|
+
lastEmit = i + 1;
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const nextCp = json.charCodeAt(i + 1);
|
|
71
|
+
|
|
72
|
+
if (nextCp === U) {
|
|
73
|
+
// Need full \uXXXX, all four digits, all hex.
|
|
74
|
+
if (
|
|
75
|
+
i + 5 < len &&
|
|
76
|
+
isHexDigit(json.charCodeAt(i + 2)) &&
|
|
77
|
+
isHexDigit(json.charCodeAt(i + 3)) &&
|
|
78
|
+
isHexDigit(json.charCodeAt(i + 4)) &&
|
|
79
|
+
isHexDigit(json.charCodeAt(i + 5))
|
|
80
|
+
) {
|
|
81
|
+
i += 6;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Truncated or non-hex \u — escape the backslash, re-process the rest.
|
|
85
|
+
parts.push(json.slice(lastEmit, i), "\\\\");
|
|
86
|
+
lastEmit = i + 1;
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (nextCp < 128 && VALID_ESCAPE_CHAR[nextCp] === 1) {
|
|
92
|
+
i += 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
parts.push(json.slice(lastEmit, i), "\\\\");
|
|
97
|
+
lastEmit = i + 1;
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Control character (cp < 0x20).
|
|
103
|
+
parts.push(json.slice(lastEmit, i), CONTROL_ESCAPES[cp]);
|
|
104
|
+
lastEmit = i + 1;
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!parts.length) return json;
|
|
109
|
+
if (lastEmit < len) parts.push(json.slice(lastEmit));
|
|
110
|
+
return parts.join("");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function parseJsonWithRepair<T>(json: string): T {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(json) as T;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const repairedJson = repairJson(json);
|
|
118
|
+
if (repairedJson !== json) {
|
|
119
|
+
return JSON.parse(repairedJson) as T;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Attempts to parse potentially incomplete JSON during streaming.
|
|
127
|
+
* Always returns a valid object, even if the JSON is incomplete.
|
|
128
|
+
*
|
|
129
|
+
* @param partialJson The partial JSON string from streaming
|
|
130
|
+
* @returns Parsed object or empty object if parsing fails
|
|
131
|
+
*/
|
|
132
|
+
export function parseStreamingJson<T = Record<string, unknown>>(partialJson: string | undefined): T {
|
|
133
|
+
partialJson = partialJson?.trimStart();
|
|
134
|
+
if (!partialJson) {
|
|
135
|
+
return {} as T;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(partialJson) as T;
|
|
139
|
+
} catch {
|
|
140
|
+
partialJson = repairJson(partialJson);
|
|
141
|
+
try {
|
|
142
|
+
return (partialParse(partialJson) ?? {}) as T;
|
|
143
|
+
} catch {
|
|
144
|
+
// If all parsing fails, return empty object
|
|
145
|
+
return {} as T;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|