@earendil-works/pi-ai 0.80.3 → 0.80.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +7 -4
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/azure-openai-responses.d.ts +1 -1
- package/dist/api/azure-openai-responses.d.ts.map +1 -1
- package/dist/api/azure-openai-responses.js +3 -1
- package/dist/api/azure-openai-responses.js.map +1 -1
- package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
- package/dist/api/bedrock-converse-stream.js +9 -5
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/google-generative-ai.d.ts.map +1 -1
- package/dist/api/google-generative-ai.js.map +1 -1
- package/dist/api/google-vertex.d.ts.map +1 -1
- package/dist/api/google-vertex.js.map +1 -1
- package/dist/api/openai-codex-responses.d.ts +1 -1
- package/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/dist/api/openai-codex-responses.js +52 -18
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts +1 -1
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +2 -1
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses-shared.d.ts.map +1 -1
- package/dist/api/openai-responses-shared.js +7 -5
- package/dist/api/openai-responses-shared.js.map +1 -1
- package/dist/api/openai-responses.d.ts +1 -1
- package/dist/api/openai-responses.d.ts.map +1 -1
- package/dist/api/openai-responses.js +3 -1
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/api/simple-options.d.ts +1 -1
- package/dist/api/simple-options.d.ts.map +1 -1
- package/dist/api/simple-options.js +1 -1
- package/dist/api/simple-options.js.map +1 -1
- package/dist/api/transform-messages.d.ts.map +1 -1
- package/dist/api/transform-messages.js +4 -1
- package/dist/api/transform-messages.js.map +1 -1
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +1242 -369
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.js +15 -6
- package/dist/models.js.map +1 -1
- package/dist/providers/amazon-bedrock.models.d.ts +100 -5
- package/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
- package/dist/providers/amazon-bedrock.models.js +70 -23
- package/dist/providers/amazon-bedrock.models.js.map +1 -1
- package/dist/providers/anthropic.models.d.ts +11 -171
- package/dist/providers/anthropic.models.d.ts.map +1 -1
- package/dist/providers/anthropic.models.js +9 -177
- package/dist/providers/anthropic.models.js.map +1 -1
- package/dist/providers/azure-openai-responses.models.d.ts +66 -0
- package/dist/providers/azure-openai-responses.models.d.ts.map +1 -1
- package/dist/providers/azure-openai-responses.models.js +54 -0
- package/dist/providers/azure-openai-responses.models.js.map +1 -1
- package/dist/providers/cerebras.models.d.ts +21 -0
- package/dist/providers/cerebras.models.d.ts.map +1 -1
- package/dist/providers/cerebras.models.js +18 -0
- package/dist/providers/cerebras.models.js.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.models.d.ts +32 -1
- package/dist/providers/cloudflare-ai-gateway.models.d.ts.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.models.js +24 -4
- package/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
- package/dist/providers/deepseek.models.d.ts +2 -2
- package/dist/providers/deepseek.models.d.ts.map +1 -1
- package/dist/providers/deepseek.models.js +2 -2
- package/dist/providers/deepseek.models.js.map +1 -1
- package/dist/providers/fireworks.models.d.ts +11 -6
- package/dist/providers/fireworks.models.d.ts.map +1 -1
- package/dist/providers/fireworks.models.js +6 -5
- package/dist/providers/fireworks.models.js.map +1 -1
- package/dist/providers/github-copilot.models.d.ts +89 -1
- package/dist/providers/github-copilot.models.d.ts.map +1 -1
- package/dist/providers/github-copilot.models.js +67 -9
- package/dist/providers/github-copilot.models.js.map +1 -1
- package/dist/providers/huggingface.models.d.ts +20 -0
- package/dist/providers/huggingface.models.d.ts.map +1 -1
- package/dist/providers/huggingface.models.js +18 -0
- package/dist/providers/huggingface.models.js.map +1 -1
- package/dist/providers/mistral.models.d.ts +1 -1
- package/dist/providers/mistral.models.d.ts.map +1 -1
- package/dist/providers/mistral.models.js +4 -4
- package/dist/providers/mistral.models.js.map +1 -1
- package/dist/providers/nvidia.models.d.ts +1 -1
- package/dist/providers/nvidia.models.d.ts.map +1 -1
- package/dist/providers/nvidia.models.js +4 -4
- package/dist/providers/nvidia.models.js.map +1 -1
- package/dist/providers/openai-codex.models.d.ts +101 -0
- package/dist/providers/openai-codex.models.d.ts.map +1 -1
- package/dist/providers/openai-codex.models.js +59 -0
- package/dist/providers/openai-codex.models.js.map +1 -1
- package/dist/providers/openai.models.d.ts +115 -0
- package/dist/providers/openai.models.d.ts.map +1 -1
- package/dist/providers/openai.models.js +61 -0
- package/dist/providers/openai.models.js.map +1 -1
- package/dist/providers/opencode-go.models.d.ts +3 -3
- package/dist/providers/opencode-go.models.d.ts.map +1 -1
- package/dist/providers/opencode-go.models.js +5 -5
- package/dist/providers/opencode-go.models.js.map +1 -1
- package/dist/providers/opencode.models.d.ts +146 -4
- package/dist/providers/opencode.models.d.ts.map +1 -1
- package/dist/providers/opencode.models.js +119 -8
- package/dist/providers/opencode.models.js.map +1 -1
- package/dist/providers/openrouter.models.d.ts +349 -24
- package/dist/providers/openrouter.models.d.ts.map +1 -1
- package/dist/providers/openrouter.models.js +351 -91
- package/dist/providers/openrouter.models.js.map +1 -1
- package/dist/providers/together.models.d.ts.map +1 -1
- package/dist/providers/together.models.js +2 -2
- package/dist/providers/together.models.js.map +1 -1
- package/dist/providers/vercel-ai-gateway.models.d.ts +129 -1
- package/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
- package/dist/providers/vercel-ai-gateway.models.js +117 -8
- package/dist/providers/vercel-ai-gateway.models.js.map +1 -1
- package/dist/providers/xai.models.d.ts +22 -0
- package/dist/providers/xai.models.d.ts.map +1 -1
- package/dist/providers/xai.models.js +18 -0
- package/dist/providers/xai.models.js.map +1 -1
- package/dist/providers/xiaomi-token-plan-ams.models.d.ts +0 -42
- package/dist/providers/xiaomi-token-plan-ams.models.d.ts.map +1 -1
- package/dist/providers/xiaomi-token-plan-ams.models.js +9 -45
- package/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
- package/dist/providers/xiaomi-token-plan-cn.models.d.ts +0 -42
- package/dist/providers/xiaomi-token-plan-cn.models.d.ts.map +1 -1
- package/dist/providers/xiaomi-token-plan-cn.models.js +9 -45
- package/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
- package/dist/providers/xiaomi-token-plan-sgp.models.d.ts +0 -42
- package/dist/providers/xiaomi-token-plan-sgp.models.d.ts.map +1 -1
- package/dist/providers/xiaomi-token-plan-sgp.models.js +9 -45
- package/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
- package/dist/providers/zai-coding-cn.models.d.ts +1 -1
- package/dist/providers/zai-coding-cn.models.d.ts.map +1 -1
- package/dist/providers/zai-coding-cn.models.js +1 -1
- package/dist/providers/zai-coding-cn.models.js.map +1 -1
- package/dist/providers/zai.models.d.ts +1 -1
- package/dist/providers/zai.models.d.ts.map +1 -1
- package/dist/providers/zai.models.js +1 -1
- package/dist/providers/zai.models.js.map +1 -1
- package/dist/types.d.ts +16 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/estimate.d.ts +3 -3
- package/dist/utils/estimate.d.ts.map +1 -1
- package/dist/utils/estimate.js +17 -9
- package/dist/utils/estimate.js.map +1 -1
- package/dist/utils/oauth/device-code.d.ts +2 -0
- package/dist/utils/oauth/device-code.d.ts.map +1 -1
- package/dist/utils/oauth/device-code.js +15 -2
- package/dist/utils/oauth/device-code.js.map +1 -1
- package/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/dist/utils/oauth/github-copilot.js +3 -2
- package/dist/utils/oauth/github-copilot.js.map +1 -1
- package/dist/utils/overflow.d.ts +1 -0
- package/dist/utils/overflow.d.ts.map +1 -1
- package/dist/utils/overflow.js +3 -0
- package/dist/utils/overflow.js.map +1 -1
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +4 -0
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +9 -21
- package/dist/utils/validation.js.map +1 -1
- package/package.json +1 -1
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { AnthropicOptions } from \"./api/anthropic-messages.ts\";\nimport type { AzureOpenAIResponsesOptions } from \"./api/azure-openai-responses.ts\";\nimport type { BedrockOptions } from \"./api/bedrock-converse-stream.ts\";\nimport type { GoogleOptions } from \"./api/google-generative-ai.ts\";\nimport type { GoogleVertexOptions } from \"./api/google-vertex.ts\";\nimport type { MistralOptions } from \"./api/mistral-conversations.ts\";\nimport type { OpenAICodexResponsesOptions } from \"./api/openai-codex-responses.ts\";\nimport type { OpenAICompletionsOptions } from \"./api/openai-completions.ts\";\nimport type { OpenAIResponsesOptions } from \"./api/openai-responses.ts\";\nimport type { AssistantMessageDiagnostic } from \"./utils/diagnostics.ts\";\nimport type { AssistantMessageEventStream } from \"./utils/event-stream.ts\";\n\nexport type { AssistantMessageEventStream } from \"./utils/event-stream.ts\";\n\nexport type KnownApi =\n\t| \"openai-completions\"\n\t| \"mistral-conversations\"\n\t| \"openai-responses\"\n\t| \"azure-openai-responses\"\n\t| \"openai-codex-responses\"\n\t| \"anthropic-messages\"\n\t| \"bedrock-converse-stream\"\n\t| \"google-generative-ai\"\n\t| \"google-vertex\";\n\nexport type Api = KnownApi | (string & {});\n\nexport type KnownImagesApi = \"openrouter-images\";\n\nexport type ImagesApi = KnownImagesApi | (string & {});\n\nexport type KnownProvider =\n\t| \"amazon-bedrock\"\n\t| \"ant-ling\"\n\t| \"anthropic\"\n\t| \"google\"\n\t| \"google-vertex\"\n\t| \"openai\"\n\t| \"azure-openai-responses\"\n\t| \"openai-codex\"\n\t| \"nvidia\"\n\t| \"deepseek\"\n\t| \"github-copilot\"\n\t| \"xai\"\n\t| \"groq\"\n\t| \"cerebras\"\n\t| \"openrouter\"\n\t| \"vercel-ai-gateway\"\n\t| \"zai\"\n\t| \"zai-coding-cn\"\n\t| \"mistral\"\n\t| \"minimax\"\n\t| \"minimax-cn\"\n\t| \"moonshotai\"\n\t| \"moonshotai-cn\"\n\t| \"huggingface\"\n\t| \"fireworks\"\n\t| \"together\"\n\t| \"opencode\"\n\t| \"opencode-go\"\n\t| \"kimi-coding\"\n\t| \"cloudflare-workers-ai\"\n\t| \"cloudflare-ai-gateway\"\n\t| \"xiaomi\"\n\t| \"xiaomi-token-plan-cn\"\n\t| \"xiaomi-token-plan-ams\"\n\t| \"xiaomi-token-plan-sgp\";\nexport type ProviderId = KnownProvider | string;\n\nexport type KnownImagesProvider = \"openrouter\";\n\nexport type ImagesProviderId = KnownImagesProvider | string;\n\nexport type ThinkingLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\nexport type ModelThinkingLevel = \"off\" | ThinkingLevel;\nexport type ThinkingLevelMap = Partial<Record<ModelThinkingLevel, string | null>>;\nexport type ChatTemplateKwargValue =\n\t| string\n\t| number\n\t| boolean\n\t| null\n\t| {\n\t\t\t$var: \"thinking.enabled\" | \"thinking.effort\";\n\t\t\tomitWhenOff?: boolean;\n\t };\n\n/** Token budgets for each thinking level (token-based providers only) */\nexport interface ThinkingBudgets {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\n// Base options all providers share\nexport type CacheRetention = \"none\" | \"short\" | \"long\";\n\nexport type Transport = \"sse\" | \"websocket\" | \"websocket-cached\" | \"auto\";\n\n/** Provider-scoped environment overrides. Values take precedence over process.env. */\nexport type ProviderEnv = Record<string, string>;\nexport type ProviderHeaders = Record<string, string | null>;\n\nexport interface ProviderResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n}\n\nexport interface StreamOptions {\n\ttemperature?: number;\n\tmaxTokens?: number;\n\tsignal?: AbortSignal;\n\tapiKey?: string;\n\t/**\n\t * Preferred transport for providers that support multiple transports.\n\t * Providers that do not support this option ignore it.\n\t */\n\ttransport?: Transport;\n\t/**\n\t * Prompt cache retention preference. Providers map this to their supported values.\n\t * Default: \"short\".\n\t */\n\tcacheRetention?: CacheRetention;\n\t/**\n\t * Optional session identifier for providers that support session-based caching.\n\t * Providers can use this to enable prompt caching, request routing, or other\n\t * session-aware features. Ignored by providers that don't support it.\n\t */\n\tsessionId?: string;\n\t/**\n\t * Optional callback for inspecting or replacing provider payloads before sending.\n\t * Return undefined to keep the payload unchanged.\n\t */\n\tonPayload?: (payload: unknown, model: Model<Api>) => unknown | undefined | Promise<unknown | undefined>;\n\t/**\n\t * Optional callback invoked after an HTTP response is received and before\n\t * its body stream is consumed.\n\t */\n\tonResponse?: (response: ProviderResponse, model: Model<Api>) => void | Promise<void>;\n\t/**\n\t * Optional custom HTTP headers to include in API requests.\n\t * Merged with provider defaults; caller values override default headers.\n\t * On AWS Bedrock these are injected via a Smithy `build`-step middleware so\n\t * they are covered by SigV4 signing; reserved headers (`x-amz-*`,\n\t * `authorization`, `host`) are silently ignored to preserve SigV4 / bearer auth.\n\t * A null value suppresses a provider/API default header with the same name.\n\t */\n\theaders?: ProviderHeaders;\n\t/**\n\t * HTTP request timeout in milliseconds for providers/SDKs that support it.\n\t * For example, OpenAI and Anthropic SDK clients default to 10 minutes.\n\t */\n\ttimeoutMs?: number;\n\t/**\n\t * WebSocket connect timeout in milliseconds for providers that support\n\t * WebSocket transports. This covers the connection/open handshake only;\n\t * stream idleness after connection uses timeoutMs.\n\t */\n\twebsocketConnectTimeoutMs?: number;\n\t/**\n\t * Maximum retry attempts for providers/SDKs that support client-side retries.\n\t * For example, OpenAI and Anthropic SDK clients default to 2.\n\t */\n\tmaxRetries?: number;\n\t/**\n\t * Maximum delay in milliseconds to wait for a retry when the server requests a long wait.\n\t * If the server's requested delay exceeds this value, the request fails immediately\n\t * with an error containing the requested delay, allowing higher-level retry logic\n\t * to handle it with user visibility.\n\t * Default: 60000 (60 seconds). Set to 0 to disable the cap.\n\t */\n\tmaxRetryDelayMs?: number;\n\t/**\n\t * Optional metadata to include in API requests.\n\t * Providers extract the fields they understand and ignore the rest.\n\t * For example, Anthropic uses `user_id` for abuse tracking and rate limiting.\n\t */\n\tmetadata?: Record<string, unknown>;\n\t/**\n\t * Provider-scoped environment values. These take precedence over process.env for\n\t * provider configuration such as regional settings, endpoint placeholders, and\n\t * proxy variables.\n\t */\n\tenv?: ProviderEnv;\n}\n\nexport type ProviderStreamOptions = StreamOptions & Record<string, unknown>;\n\n/**\n * Maps known APIs to their full provider-specific stream option types.\n * Type-only imports from API implementation modules are erased at emit, so\n * this is tree-shake safe.\n */\nexport interface ApiOptionsMap {\n\t\"anthropic-messages\": AnthropicOptions;\n\t\"openai-completions\": OpenAICompletionsOptions;\n\t\"openai-responses\": OpenAIResponsesOptions;\n\t\"openai-codex-responses\": OpenAICodexResponsesOptions;\n\t\"azure-openai-responses\": AzureOpenAIResponsesOptions;\n\t\"google-generative-ai\": GoogleOptions;\n\t\"google-vertex\": GoogleVertexOptions;\n\t\"mistral-conversations\": MistralOptions;\n\t\"bedrock-converse-stream\": BedrockOptions;\n}\n\n/**\n * Full stream options for an API. Known APIs resolve to their concrete option\n * type; custom API strings fall back to the generic shape.\n */\nexport type ApiStreamOptions<TApi extends Api> = TApi extends keyof ApiOptionsMap\n\t? ApiOptionsMap[TApi]\n\t: StreamOptions & Record<string, unknown>;\n\n/**\n * The uniform stream contract of an API implementation module: every module\n * under `src/api/` exports exactly `stream` and `streamSimple`, so the module\n * itself satisfies this interface. Lazy wrappers (`lazyApi()`) and provider\n * factories pass these around as values. This is the untyped dispatch shape;\n * per-API option typing lives on the implementation modules themselves and on\n * `Provider.stream()` via `ApiStreamOptions`.\n */\nexport interface ProviderStreams {\n\tstream(model: Model<Api>, context: Context, options?: StreamOptions): AssistantMessageEventStream;\n\tstreamSimple(model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream;\n}\n\n/**\n * The uniform contract of an image-generation API implementation module:\n * every image API module under `src/api/` exports exactly `generateImages`,\n * so the module itself satisfies this interface. Lazy wrappers and image\n * provider factories pass these around as values.\n */\nexport interface ProviderImages {\n\tgenerateImages(\n\t\tmodel: ImagesModel<ImagesApi>,\n\t\tcontext: ImagesContext,\n\t\toptions?: ImagesOptions,\n\t): Promise<AssistantImages>;\n}\n\nexport interface ImagesOptions {\n\tsignal?: AbortSignal;\n\tapiKey?: string;\n\t/**\n\t * Provider-scoped environment values. These take precedence over process.env for\n\t * provider configuration such as endpoint placeholders and proxy variables.\n\t */\n\tenv?: ProviderEnv;\n\t/**\n\t * Optional callback for inspecting or replacing provider payloads before sending.\n\t * Return undefined to keep the payload unchanged.\n\t */\n\tonPayload?: (payload: unknown, model: ImagesModel<ImagesApi>) => unknown | undefined | Promise<unknown | undefined>;\n\t/**\n\t * Optional callback invoked after an HTTP response is received.\n\t */\n\tonResponse?: (response: ProviderResponse, model: ImagesModel<ImagesApi>) => void | Promise<void>;\n\t/**\n\t * Optional custom HTTP headers to include in API requests.\n\t * Merged with provider defaults; can override default headers.\n\t * A null value suppresses a provider/API default header with the same name.\n\t */\n\theaders?: ProviderHeaders;\n\t/**\n\t * HTTP request timeout in milliseconds for providers/SDKs that support it.\n\t */\n\ttimeoutMs?: number;\n\t/**\n\t * Maximum retry attempts for providers/SDKs that support client-side retries.\n\t */\n\tmaxRetries?: number;\n\t/**\n\t * Maximum delay in milliseconds to wait for a retry when the server requests a long wait.\n\t * If the server's requested delay exceeds this value, the request fails immediately\n\t * with an error containing the requested delay, allowing higher-level retry logic\n\t * to handle it with user visibility.\n\t * Default: 60000 (60 seconds). Set to 0 to disable the cap.\n\t */\n\tmaxRetryDelayMs?: number;\n\t/**\n\t * Optional metadata to include in API requests.\n\t * Providers extract the fields they understand and ignore the rest.\n\t */\n\tmetadata?: Record<string, unknown>;\n}\n\nexport type ProviderImagesOptions = ImagesOptions & Record<string, unknown>;\n\n// Unified options with reasoning passed to streamSimple() and completeSimple()\nexport interface SimpleStreamOptions extends StreamOptions {\n\treasoning?: ThinkingLevel;\n\t/** Custom token budgets for thinking levels (token-based providers only) */\n\tthinkingBudgets?: ThinkingBudgets;\n}\n\n// Generic StreamFunction with typed options.\n//\n// Contract:\n// - Must return an AssistantMessageEventStream.\n// - Once invoked, request/model/runtime failures should be encoded in the\n// returned stream, not thrown.\n// - Error termination must produce an AssistantMessage with stopReason\n// \"error\" or \"aborted\" and errorMessage, emitted via the stream protocol.\nexport type StreamFunction<TApi extends Api = Api, TOptions extends StreamOptions = StreamOptions> = (\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: TOptions,\n) => AssistantMessageEventStream;\n\nexport type ImagesFunction<TApi extends ImagesApi = ImagesApi, TOptions extends ImagesOptions = ImagesOptions> = (\n\tmodel: ImagesModel<TApi>,\n\tcontext: ImagesContext,\n\toptions?: TOptions,\n) => Promise<AssistantImages>;\n\nexport interface TextSignatureV1 {\n\tv: 1;\n\tid: string;\n\tphase?: \"commentary\" | \"final_answer\";\n}\n\nexport interface TextContent {\n\ttype: \"text\";\n\ttext: string;\n\ttextSignature?: string; // e.g., for OpenAI responses, message metadata (legacy id string or TextSignatureV1 JSON)\n}\n\nexport interface ThinkingContent {\n\ttype: \"thinking\";\n\tthinking: string;\n\tthinkingSignature?: string; // e.g., for OpenAI responses, the reasoning item ID\n\t/** When true, the thinking content was redacted by safety filters. The opaque\n\t * encrypted payload is stored in `thinkingSignature` so it can be passed back\n\t * to the API for multi-turn continuity. */\n\tredacted?: boolean;\n}\n\nexport interface ImageContent {\n\ttype: \"image\";\n\tdata: string; // base64 encoded image data\n\tmimeType: string; // e.g., \"image/jpeg\", \"image/png\"\n}\n\nexport interface ToolCall {\n\ttype: \"toolCall\";\n\tid: string;\n\tname: string;\n\targuments: Record<string, any>;\n\tthoughtSignature?: string; // Google-specific: opaque signature for reusing thought context\n}\n\nexport interface Usage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\t/** Subset of `cacheWrite` written with 1h retention. Only Anthropic reports this split. */\n\tcacheWrite1h?: number;\n\t/**\n\t * Reasoning/thinking tokens, when the provider reports them. This is a subset of\n\t * `output`: `output` already includes these tokens. Set to a number (possibly 0) by\n\t * providers that expose a reasoning breakdown; left undefined by providers that don't.\n\t */\n\treasoning?: number;\n\ttotalTokens: number;\n\tcost: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n}\n\nexport type StopReason = \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\";\n\nexport interface UserMessage {\n\trole: \"user\";\n\tcontent: string | (TextContent | ImageContent)[];\n\ttimestamp: number; // Unix timestamp in milliseconds\n}\n\nexport interface AssistantMessage {\n\trole: \"assistant\";\n\tcontent: (TextContent | ThinkingContent | ToolCall)[];\n\tapi: Api;\n\tprovider: ProviderId;\n\tmodel: string;\n\tresponseModel?: string; // Concrete `chunk.model` when different from the requested `model` (e.g. OpenRouter `auto` -> `anthropic/...`)\n\tresponseId?: string; // Provider-specific response/message identifier when the upstream API exposes one\n\tdiagnostics?: AssistantMessageDiagnostic[]; // Redacted provider/runtime diagnostics for failures and recoveries.\n\tusage: Usage;\n\tstopReason: StopReason;\n\terrorMessage?: string;\n\ttimestamp: number; // Unix timestamp in milliseconds\n}\n\nexport interface ToolResultMessage<TDetails = any> {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: (TextContent | ImageContent)[]; // Supports text and images\n\tdetails?: TDetails;\n\tisError: boolean;\n\ttimestamp: number; // Unix timestamp in milliseconds\n}\n\nexport type Message = UserMessage | AssistantMessage | ToolResultMessage;\n\nexport type ImagesInputContent = TextContent | ImageContent;\nexport type ImagesOutputContent = TextContent | ImageContent;\n\nexport interface ImagesContext {\n\tinput: ImagesInputContent[];\n}\n\nexport type ImagesStopReason = \"stop\" | \"error\" | \"aborted\";\n\nexport interface AssistantImages {\n\tapi: ImagesApi;\n\tprovider: ImagesProviderId;\n\tmodel: string;\n\toutput: ImagesOutputContent[];\n\tresponseId?: string;\n\tusage?: Usage;\n\tstopReason: ImagesStopReason;\n\terrorMessage?: string;\n\ttimestamp: number; // Unix timestamp in milliseconds\n}\n\nimport type { TSchema } from \"typebox\";\n\nexport interface Tool<TParameters extends TSchema = TSchema> {\n\tname: string;\n\tdescription: string;\n\tparameters: TParameters;\n}\n\nexport interface Context {\n\tsystemPrompt?: string;\n\tmessages: Message[];\n\ttools?: Tool[];\n}\n\n/**\n * Event protocol for AssistantMessageEventStream.\n *\n * Streams should emit `start` before partial updates, then terminate with either:\n * - `done` carrying the final successful AssistantMessage, or\n * - `error` carrying the final AssistantMessage with stopReason \"error\" or \"aborted\"\n * and errorMessage.\n */\nexport type AssistantMessageEvent =\n\t| { type: \"start\"; partial: AssistantMessage }\n\t| { type: \"text_start\"; contentIndex: number; partial: AssistantMessage }\n\t| { type: \"text_delta\"; contentIndex: number; delta: string; partial: AssistantMessage }\n\t| { type: \"text_end\"; contentIndex: number; content: string; partial: AssistantMessage }\n\t| { type: \"thinking_start\"; contentIndex: number; partial: AssistantMessage }\n\t| { type: \"thinking_delta\"; contentIndex: number; delta: string; partial: AssistantMessage }\n\t| { type: \"thinking_end\"; contentIndex: number; content: string; partial: AssistantMessage }\n\t| { type: \"toolcall_start\"; contentIndex: number; partial: AssistantMessage }\n\t| { type: \"toolcall_delta\"; contentIndex: number; delta: string; partial: AssistantMessage }\n\t| { type: \"toolcall_end\"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage }\n\t| { type: \"done\"; reason: Extract<StopReason, \"stop\" | \"length\" | \"toolUse\">; message: AssistantMessage }\n\t| { type: \"error\"; reason: Extract<StopReason, \"aborted\" | \"error\">; error: AssistantMessage };\n\n/**\n * Compatibility settings for OpenAI-compatible completions APIs.\n * Use this to override URL-based auto-detection for custom providers.\n */\nexport interface OpenAICompletionsCompat {\n\t/** Whether the provider supports the `store` field. Default: auto-detected from URL. */\n\tsupportsStore?: boolean;\n\t/** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */\n\tsupportsDeveloperRole?: boolean;\n\t/** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */\n\tsupportsReasoningEffort?: boolean;\n\t/** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */\n\tsupportsUsageInStreaming?: boolean;\n\t/** Which field to use for max tokens. Default: auto-detected from URL. */\n\tmaxTokensField?: \"max_completion_tokens\" | \"max_tokens\";\n\t/** Whether tool results require the `name` field. Default: auto-detected from URL. */\n\trequiresToolResultName?: boolean;\n\t/** Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. */\n\trequiresAssistantAfterToolResult?: boolean;\n\t/** Whether thinking blocks must be converted to text blocks with <thinking> delimiters. Default: auto-detected from URL. */\n\trequiresThinkingAsText?: boolean;\n\t/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */\n\trequiresReasoningContentOnAssistantMessages?: boolean;\n\t/** Format for reasoning/thinking parameter. \"openai\" uses reasoning_effort, \"openrouter\" uses reasoning: { effort }, \"deepseek\" uses thinking: { type } plus reasoning_effort when supported, \"together\" uses reasoning: { enabled } plus reasoning_effort when supported, \"zai\" uses thinking: { type }, \"qwen\" uses top-level enable_thinking: boolean, \"qwen-chat-template\" uses chat_template_kwargs.enable_thinking and preserve_thinking, \"chat-template\" uses configurable chat_template_kwargs, \"string-thinking\" uses top-level thinking: string, and \"ant-ling\" uses reasoning: { effort } only when the mapped effort is non-null. Default: \"openai\". */\n\tthinkingFormat?:\n\t\t| \"openai\"\n\t\t| \"openrouter\"\n\t\t| \"deepseek\"\n\t\t| \"together\"\n\t\t| \"zai\"\n\t\t| \"qwen\"\n\t\t| \"chat-template\"\n\t\t| \"qwen-chat-template\"\n\t\t| \"string-thinking\"\n\t\t| \"ant-ling\";\n\t/** Kwargs to send as `chat_template_kwargs` when `thinkingFormat` is `chat-template`. Use `{ \"$var\": \"thinking.enabled\" }` or `{ \"$var\": \"thinking.effort\" }` for pi-controlled thinking values. */\n\tchatTemplateKwargs?: Record<string, ChatTemplateKwargValue>;\n\t/** OpenRouter-compatible routing preferences sent as the `provider` request field. */\n\topenRouterRouting?: OpenRouterRouting;\n\t/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */\n\tvercelGatewayRouting?: VercelGatewayRouting;\n\t/** Whether z.ai supports top-level `tool_stream: true` for streaming tool call deltas. Default: false. */\n\tzaiToolStream?: boolean;\n\t/** Whether the provider supports the `strict` field in tool definitions. Default: true. */\n\tsupportsStrictMode?: boolean;\n\t/** Cache control convention for prompt caching. \"anthropic\" applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content. */\n\tcacheControlFormat?: \"anthropic\";\n\t/** Whether to send known session-affinity headers (`session_id`, `x-client-request-id`, `x-session-affinity`) from `options.sessionId` when caching is enabled. Default: false. */\n\tsendSessionAffinityHeaders?: boolean;\n\t/** Whether the provider supports long prompt cache retention (`prompt_cache_retention: \"24h\"` or Anthropic-style `cache_control.ttl: \"1h\"`, depending on format). Default: true. */\n\tsupportsLongCacheRetention?: boolean;\n}\n\n/** Compatibility settings for OpenAI Responses APIs. */\nexport interface OpenAIResponsesCompat {\n\t/** Whether the provider supports the `developer` role (vs `system`). Default: true. */\n\tsupportsDeveloperRole?: boolean;\n\t/** Whether to send the OpenAI `session_id` cache-affinity header from `options.sessionId` when caching is enabled. Default: true. */\n\tsendSessionIdHeader?: boolean;\n\t/** Whether the provider supports `prompt_cache_retention: \"24h\"`. Default: true. */\n\tsupportsLongCacheRetention?: boolean;\n}\n\n/** Compatibility settings for Anthropic Messages-compatible APIs. */\nexport interface AnthropicMessagesCompat {\n\t/**\n\t * Whether the provider accepts per-tool `eager_input_streaming`.\n\t * When false, the Anthropic provider omits `tools[].eager_input_streaming`\n\t * and sends the legacy `fine-grained-tool-streaming-2025-05-14` beta header\n\t * for tool-enabled requests.\n\t * Default: true.\n\t */\n\tsupportsEagerToolInputStreaming?: boolean;\n\t/** Whether the provider supports Anthropic long cache retention (`cache_control.ttl: \"1h\"`). Default: true. */\n\tsupportsLongCacheRetention?: boolean;\n\t/**\n\t * Whether to send the `x-session-affinity` header from `options.sessionId`\n\t * when caching is enabled. Required for providers like Fireworks that use\n\t * session affinity for prompt cache routing (requests to the same replica\n\t * maximize cache hits).\n\t * Default: false.\n\t */\n\tsendSessionAffinityHeaders?: boolean;\n\t/**\n\t * Whether the provider supports Anthropic-style `cache_control` markers on\n\t * tool definitions. When false, `cache_control` is omitted from tool params.\n\t * Some Anthropic-compatible providers (e.g., Fireworks) do not support this\n\t * field on tools and may reject or ignore it.\n\t * Default: true.\n\t */\n\tsupportsCacheControlOnTools?: boolean;\n\t/**\n\t * Whether the model accepts the Anthropic `temperature` request field.\n\t * Claude Opus 4.7+ rejects non-default temperature values.\n\t * Default: true.\n\t */\n\tsupportsTemperature?: boolean;\n\t/**\n\t * Whether to force adaptive thinking (`thinking.type: \"adaptive\"` plus\n\t * `output_config.effort`) regardless of the model id. Built-in models that\n\t * require adaptive thinking set this in generated metadata. Custom\n\t * Anthropic-compatible providers can set this to `true` for any model whose\n\t * upstream requires the adaptive format. Set to `false` to\n\t * opt out on overridden built-in models.\n\t * Default: false.\n\t */\n\tforceAdaptiveThinking?: boolean;\n\t/** Whether to replay empty thinking signatures as `signature: \"\"` instead of converting thinking to text. Default: false. */\n\tallowEmptySignature?: boolean;\n}\n\n/**\n * OpenRouter provider routing preferences.\n * Controls which upstream providers OpenRouter routes requests to.\n * Sent as the `provider` field in the OpenRouter API request body.\n * @see https://openrouter.ai/docs/guides/routing/provider-selection\n */\nexport interface OpenRouterRouting {\n\t/** Whether to allow backup providers to serve requests. Default: true. */\n\tallow_fallbacks?: boolean;\n\t/** Whether to filter providers to only those that support all parameters in the request. Default: false. */\n\trequire_parameters?: boolean;\n\t/** Data collection setting. \"allow\" (default): allow providers that may store/train on data. \"deny\": only use providers that don't collect user data. */\n\tdata_collection?: \"deny\" | \"allow\";\n\t/** Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. */\n\tzdr?: boolean;\n\t/** Whether to restrict routing to only models that allow text distillation. */\n\tenforce_distillable_text?: boolean;\n\t/** An ordered list of provider names/slugs to try in sequence, falling back to the next if unavailable. */\n\torder?: string[];\n\t/** List of provider names/slugs to exclusively allow for this request. */\n\tonly?: string[];\n\t/** List of provider names/slugs to skip for this request. */\n\tignore?: string[];\n\t/** A list of quantization levels to filter providers by (e.g., [\"fp16\", \"bf16\", \"fp8\", \"fp6\", \"int8\", \"int4\", \"fp4\", \"fp32\"]). */\n\tquantizations?: string[];\n\t/** Sorting strategy. Can be a string (e.g., \"price\", \"throughput\", \"latency\") or an object with `by` and `partition`. */\n\tsort?:\n\t\t| string\n\t\t| {\n\t\t\t\t/** The sorting metric: \"price\", \"throughput\", \"latency\". */\n\t\t\t\tby?: string;\n\t\t\t\t/** Partitioning strategy: \"model\" (default) or \"none\". */\n\t\t\t\tpartition?: string | null;\n\t\t };\n\t/** Maximum price per million tokens (USD). */\n\tmax_price?: {\n\t\t/** Price per million prompt tokens. */\n\t\tprompt?: number | string;\n\t\t/** Price per million completion tokens. */\n\t\tcompletion?: number | string;\n\t\t/** Price per image. */\n\t\timage?: number | string;\n\t\t/** Price per audio unit. */\n\t\taudio?: number | string;\n\t\t/** Price per request. */\n\t\trequest?: number | string;\n\t};\n\t/** Preferred minimum throughput (tokens/second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. */\n\tpreferred_min_throughput?:\n\t\t| number\n\t\t| {\n\t\t\t\t/** Minimum tokens/second at the 50th percentile. */\n\t\t\t\tp50?: number;\n\t\t\t\t/** Minimum tokens/second at the 75th percentile. */\n\t\t\t\tp75?: number;\n\t\t\t\t/** Minimum tokens/second at the 90th percentile. */\n\t\t\t\tp90?: number;\n\t\t\t\t/** Minimum tokens/second at the 99th percentile. */\n\t\t\t\tp99?: number;\n\t\t };\n\t/** Preferred maximum latency (seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. */\n\tpreferred_max_latency?:\n\t\t| number\n\t\t| {\n\t\t\t\t/** Maximum latency in seconds at the 50th percentile. */\n\t\t\t\tp50?: number;\n\t\t\t\t/** Maximum latency in seconds at the 75th percentile. */\n\t\t\t\tp75?: number;\n\t\t\t\t/** Maximum latency in seconds at the 90th percentile. */\n\t\t\t\tp90?: number;\n\t\t\t\t/** Maximum latency in seconds at the 99th percentile. */\n\t\t\t\tp99?: number;\n\t\t };\n}\n\n/**\n * Vercel AI Gateway routing preferences.\n * Controls which upstream providers the gateway routes requests to.\n * @see https://vercel.com/docs/ai-gateway/models-and-providers/provider-options\n */\nexport interface VercelGatewayRouting {\n\t/** List of provider slugs to exclusively use for this request (e.g., [\"bedrock\", \"anthropic\"]). */\n\tonly?: string[];\n\t/** List of provider slugs to try in order (e.g., [\"anthropic\", \"openai\"]). */\n\torder?: string[];\n}\n\n// Model interface for the unified model system\nexport interface Model<TApi extends Api> {\n\tid: string;\n\tname: string;\n\tapi: TApi;\n\tprovider: ProviderId;\n\tbaseUrl: string;\n\treasoning: boolean;\n\t/**\n\t * Maps pi thinking levels to provider/model-specific values.\n\t * Missing keys use provider defaults. null marks a level as unsupported.\n\t */\n\tthinkingLevelMap?: ThinkingLevelMap;\n\tinput: (\"text\" | \"image\")[];\n\tcost: {\n\t\tinput: number; // $/million tokens\n\t\toutput: number; // $/million tokens\n\t\tcacheRead: number; // $/million tokens\n\t\tcacheWrite: number; // $/million tokens\n\t};\n\tcontextWindow: number;\n\tmaxTokens: number;\n\theaders?: Record<string, string>;\n\t/** Compatibility overrides for OpenAI-compatible APIs. If not set, auto-detected from baseUrl. */\n\tcompat?: TApi extends \"openai-completions\"\n\t\t? OpenAICompletionsCompat\n\t\t: TApi extends \"openai-responses\"\n\t\t\t? OpenAIResponsesCompat\n\t\t\t: TApi extends \"anthropic-messages\"\n\t\t\t\t? AnthropicMessagesCompat\n\t\t\t\t: never;\n}\n\nexport interface ImagesModel<TApi extends ImagesApi>\n\textends Omit<Model<Api>, \"api\" | \"provider\" | \"reasoning\" | \"contextWindow\" | \"maxTokens\" | \"compat\"> {\n\tapi: TApi;\n\tprovider: ImagesProviderId;\n\toutput: (\"text\" | \"image\")[];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { AnthropicOptions } from \"./api/anthropic-messages.ts\";\nimport type { AzureOpenAIResponsesOptions } from \"./api/azure-openai-responses.ts\";\nimport type { BedrockOptions } from \"./api/bedrock-converse-stream.ts\";\nimport type { GoogleOptions } from \"./api/google-generative-ai.ts\";\nimport type { GoogleVertexOptions } from \"./api/google-vertex.ts\";\nimport type { MistralOptions } from \"./api/mistral-conversations.ts\";\nimport type { OpenAICodexResponsesOptions } from \"./api/openai-codex-responses.ts\";\nimport type { OpenAICompletionsOptions } from \"./api/openai-completions.ts\";\nimport type { OpenAIResponsesOptions } from \"./api/openai-responses.ts\";\nimport type { AssistantMessageDiagnostic } from \"./utils/diagnostics.ts\";\nimport type { AssistantMessageEventStream } from \"./utils/event-stream.ts\";\n\nexport type { AssistantMessageEventStream } from \"./utils/event-stream.ts\";\n\nexport type KnownApi =\n\t| \"openai-completions\"\n\t| \"mistral-conversations\"\n\t| \"openai-responses\"\n\t| \"azure-openai-responses\"\n\t| \"openai-codex-responses\"\n\t| \"anthropic-messages\"\n\t| \"bedrock-converse-stream\"\n\t| \"google-generative-ai\"\n\t| \"google-vertex\";\n\nexport type Api = KnownApi | (string & {});\n\nexport type KnownImagesApi = \"openrouter-images\";\n\nexport type ImagesApi = KnownImagesApi | (string & {});\n\nexport type KnownProvider =\n\t| \"amazon-bedrock\"\n\t| \"ant-ling\"\n\t| \"anthropic\"\n\t| \"google\"\n\t| \"google-vertex\"\n\t| \"openai\"\n\t| \"azure-openai-responses\"\n\t| \"openai-codex\"\n\t| \"nvidia\"\n\t| \"deepseek\"\n\t| \"github-copilot\"\n\t| \"xai\"\n\t| \"groq\"\n\t| \"cerebras\"\n\t| \"openrouter\"\n\t| \"vercel-ai-gateway\"\n\t| \"zai\"\n\t| \"zai-coding-cn\"\n\t| \"mistral\"\n\t| \"minimax\"\n\t| \"minimax-cn\"\n\t| \"moonshotai\"\n\t| \"moonshotai-cn\"\n\t| \"huggingface\"\n\t| \"fireworks\"\n\t| \"together\"\n\t| \"opencode\"\n\t| \"opencode-go\"\n\t| \"kimi-coding\"\n\t| \"cloudflare-workers-ai\"\n\t| \"cloudflare-ai-gateway\"\n\t| \"xiaomi\"\n\t| \"xiaomi-token-plan-cn\"\n\t| \"xiaomi-token-plan-ams\"\n\t| \"xiaomi-token-plan-sgp\";\nexport type ProviderId = KnownProvider | string;\n\nexport type KnownImagesProvider = \"openrouter\";\n\nexport type ImagesProviderId = KnownImagesProvider | string;\n\nexport type ThinkingLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\nexport type ModelThinkingLevel = \"off\" | ThinkingLevel;\nexport type ThinkingLevelMap = Partial<Record<ModelThinkingLevel, string | null>>;\nexport type ChatTemplateKwargValue =\n\t| string\n\t| number\n\t| boolean\n\t| null\n\t| {\n\t\t\t$var: \"thinking.enabled\" | \"thinking.effort\";\n\t\t\tomitWhenOff?: boolean;\n\t };\n\n/** Token budgets for each thinking level (token-based providers only) */\nexport interface ThinkingBudgets {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\n// Base options all providers share\nexport type CacheRetention = \"none\" | \"short\" | \"long\";\n\nexport type Transport = \"sse\" | \"websocket\" | \"websocket-cached\" | \"auto\";\n\n/** Provider-scoped environment overrides. Values take precedence over process.env. */\nexport type ProviderEnv = Record<string, string>;\nexport type ProviderHeaders = Record<string, string | null>;\n\nexport interface ProviderResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n}\n\nexport interface StreamOptions {\n\ttemperature?: number;\n\tmaxTokens?: number;\n\tsignal?: AbortSignal;\n\tapiKey?: string;\n\t/**\n\t * Preferred transport for providers that support multiple transports.\n\t * Providers that do not support this option ignore it.\n\t */\n\ttransport?: Transport;\n\t/**\n\t * Prompt cache retention preference. Providers map this to their supported values.\n\t * Default: \"short\".\n\t */\n\tcacheRetention?: CacheRetention;\n\t/**\n\t * Optional session identifier for providers that support session-based caching.\n\t * Providers can use this to enable prompt caching, request routing, or other\n\t * session-aware features. Ignored by providers that don't support it.\n\t */\n\tsessionId?: string;\n\t/**\n\t * Optional callback for inspecting or replacing provider payloads before sending.\n\t * Return undefined to keep the payload unchanged.\n\t */\n\tonPayload?: (payload: unknown, model: Model<Api>) => unknown | undefined | Promise<unknown | undefined>;\n\t/**\n\t * Optional callback invoked after an HTTP response is received and before\n\t * its body stream is consumed.\n\t */\n\tonResponse?: (response: ProviderResponse, model: Model<Api>) => void | Promise<void>;\n\t/**\n\t * Optional custom HTTP headers to include in API requests.\n\t * Merged with provider defaults; caller values override default headers.\n\t * On AWS Bedrock these are injected via a Smithy `build`-step middleware so\n\t * they are covered by SigV4 signing; reserved headers (`x-amz-*`,\n\t * `authorization`, `host`) are silently ignored to preserve SigV4 / bearer auth.\n\t * A null value suppresses a provider/API default header with the same name.\n\t */\n\theaders?: ProviderHeaders;\n\t/**\n\t * HTTP request timeout in milliseconds for providers/SDKs that support it.\n\t * For example, OpenAI and Anthropic SDK clients default to 10 minutes.\n\t */\n\ttimeoutMs?: number;\n\t/**\n\t * WebSocket connect timeout in milliseconds for providers that support\n\t * WebSocket transports. This covers the connection/open handshake only;\n\t * stream idleness after connection uses timeoutMs.\n\t */\n\twebsocketConnectTimeoutMs?: number;\n\t/**\n\t * Maximum retry attempts for providers/SDKs that support client-side retries.\n\t * For example, OpenAI and Anthropic SDK clients default to 2.\n\t */\n\tmaxRetries?: number;\n\t/**\n\t * Maximum delay in milliseconds to wait for a retry when the server requests a long wait.\n\t * If the server's requested delay exceeds this value, the request fails immediately\n\t * with an error containing the requested delay, allowing higher-level retry logic\n\t * to handle it with user visibility.\n\t * Default: 60000 (60 seconds). Set to 0 to disable the cap.\n\t */\n\tmaxRetryDelayMs?: number;\n\t/**\n\t * Optional metadata to include in API requests.\n\t * Providers extract the fields they understand and ignore the rest.\n\t * For example, Anthropic uses `user_id` for abuse tracking and rate limiting.\n\t */\n\tmetadata?: Record<string, unknown>;\n\t/**\n\t * Provider-scoped environment values. These take precedence over process.env for\n\t * provider configuration such as regional settings, endpoint placeholders, and\n\t * proxy variables.\n\t */\n\tenv?: ProviderEnv;\n}\n\nexport type ProviderStreamOptions = StreamOptions & Record<string, unknown>;\n\n/**\n * Maps known APIs to their full provider-specific stream option types.\n * Type-only imports from API implementation modules are erased at emit, so\n * this is tree-shake safe.\n */\nexport interface ApiOptionsMap {\n\t\"anthropic-messages\": AnthropicOptions;\n\t\"openai-completions\": OpenAICompletionsOptions;\n\t\"openai-responses\": OpenAIResponsesOptions;\n\t\"openai-codex-responses\": OpenAICodexResponsesOptions;\n\t\"azure-openai-responses\": AzureOpenAIResponsesOptions;\n\t\"google-generative-ai\": GoogleOptions;\n\t\"google-vertex\": GoogleVertexOptions;\n\t\"mistral-conversations\": MistralOptions;\n\t\"bedrock-converse-stream\": BedrockOptions;\n}\n\n/**\n * Full stream options for an API. Known APIs resolve to their concrete option\n * type; custom API strings fall back to the generic shape.\n */\nexport type ApiStreamOptions<TApi extends Api> = TApi extends keyof ApiOptionsMap\n\t? ApiOptionsMap[TApi]\n\t: StreamOptions & Record<string, unknown>;\n\n/**\n * The uniform stream contract of an API implementation module: every module\n * under `src/api/` exports exactly `stream` and `streamSimple`, so the module\n * itself satisfies this interface. Lazy wrappers (`lazyApi()`) and provider\n * factories pass these around as values. This is the untyped dispatch shape;\n * per-API option typing lives on the implementation modules themselves and on\n * `Provider.stream()` via `ApiStreamOptions`.\n */\nexport interface ProviderStreams {\n\tstream(model: Model<Api>, context: Context, options?: StreamOptions): AssistantMessageEventStream;\n\tstreamSimple(model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream;\n}\n\n/**\n * The uniform contract of an image-generation API implementation module:\n * every image API module under `src/api/` exports exactly `generateImages`,\n * so the module itself satisfies this interface. Lazy wrappers and image\n * provider factories pass these around as values.\n */\nexport interface ProviderImages {\n\tgenerateImages(\n\t\tmodel: ImagesModel<ImagesApi>,\n\t\tcontext: ImagesContext,\n\t\toptions?: ImagesOptions,\n\t): Promise<AssistantImages>;\n}\n\nexport interface ImagesOptions {\n\tsignal?: AbortSignal;\n\tapiKey?: string;\n\t/**\n\t * Provider-scoped environment values. These take precedence over process.env for\n\t * provider configuration such as endpoint placeholders and proxy variables.\n\t */\n\tenv?: ProviderEnv;\n\t/**\n\t * Optional callback for inspecting or replacing provider payloads before sending.\n\t * Return undefined to keep the payload unchanged.\n\t */\n\tonPayload?: (payload: unknown, model: ImagesModel<ImagesApi>) => unknown | undefined | Promise<unknown | undefined>;\n\t/**\n\t * Optional callback invoked after an HTTP response is received.\n\t */\n\tonResponse?: (response: ProviderResponse, model: ImagesModel<ImagesApi>) => void | Promise<void>;\n\t/**\n\t * Optional custom HTTP headers to include in API requests.\n\t * Merged with provider defaults; can override default headers.\n\t * A null value suppresses a provider/API default header with the same name.\n\t */\n\theaders?: ProviderHeaders;\n\t/**\n\t * HTTP request timeout in milliseconds for providers/SDKs that support it.\n\t */\n\ttimeoutMs?: number;\n\t/**\n\t * Maximum retry attempts for providers/SDKs that support client-side retries.\n\t */\n\tmaxRetries?: number;\n\t/**\n\t * Maximum delay in milliseconds to wait for a retry when the server requests a long wait.\n\t * If the server's requested delay exceeds this value, the request fails immediately\n\t * with an error containing the requested delay, allowing higher-level retry logic\n\t * to handle it with user visibility.\n\t * Default: 60000 (60 seconds). Set to 0 to disable the cap.\n\t */\n\tmaxRetryDelayMs?: number;\n\t/**\n\t * Optional metadata to include in API requests.\n\t * Providers extract the fields they understand and ignore the rest.\n\t */\n\tmetadata?: Record<string, unknown>;\n}\n\nexport type ProviderImagesOptions = ImagesOptions & Record<string, unknown>;\n\n// Unified options with reasoning passed to streamSimple() and completeSimple()\nexport interface SimpleStreamOptions extends StreamOptions {\n\treasoning?: ThinkingLevel;\n\t/** Custom token budgets for thinking levels (token-based providers only) */\n\tthinkingBudgets?: ThinkingBudgets;\n}\n\n// Generic StreamFunction with typed options.\n//\n// Contract:\n// - Must return an AssistantMessageEventStream.\n// - Once invoked, request/model/runtime failures should be encoded in the\n// returned stream, not thrown.\n// - Error termination must produce an AssistantMessage with stopReason\n// \"error\" or \"aborted\" and errorMessage, emitted via the stream protocol.\nexport type StreamFunction<TApi extends Api = Api, TOptions extends StreamOptions = StreamOptions> = (\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: TOptions,\n) => AssistantMessageEventStream;\n\nexport type ImagesFunction<TApi extends ImagesApi = ImagesApi, TOptions extends ImagesOptions = ImagesOptions> = (\n\tmodel: ImagesModel<TApi>,\n\tcontext: ImagesContext,\n\toptions?: TOptions,\n) => Promise<AssistantImages>;\n\nexport interface TextSignatureV1 {\n\tv: 1;\n\tid: string;\n\tphase?: \"commentary\" | \"final_answer\";\n}\n\nexport interface TextContent {\n\ttype: \"text\";\n\ttext: string;\n\ttextSignature?: string; // e.g., for OpenAI responses, message metadata (legacy id string or TextSignatureV1 JSON)\n}\n\nexport interface ThinkingContent {\n\ttype: \"thinking\";\n\tthinking: string;\n\tthinkingSignature?: string; // e.g., for OpenAI responses, the reasoning item ID\n\t/** When true, the thinking content was redacted by safety filters. The opaque\n\t * encrypted payload is stored in `thinkingSignature` so it can be passed back\n\t * to the API for multi-turn continuity. */\n\tredacted?: boolean;\n}\n\nexport interface ImageContent {\n\ttype: \"image\";\n\tdata: string; // base64 encoded image data\n\tmimeType: string; // e.g., \"image/jpeg\", \"image/png\"\n}\n\nexport interface ToolCall {\n\ttype: \"toolCall\";\n\tid: string;\n\tname: string;\n\targuments: Record<string, any>;\n\tthoughtSignature?: string; // Google-specific: opaque signature for reusing thought context\n}\n\nexport interface Usage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\t/** Subset of `cacheWrite` written with 1h retention. Only Anthropic reports this split. */\n\tcacheWrite1h?: number;\n\t/**\n\t * Reasoning/thinking tokens, when the provider reports them. This is a subset of\n\t * `output`: `output` already includes these tokens. Set to a number (possibly 0) by\n\t * providers that expose a reasoning breakdown; left undefined by providers that don't.\n\t */\n\treasoning?: number;\n\ttotalTokens: number;\n\tcost: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n}\n\nexport type StopReason = \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\";\n\nexport interface UserMessage {\n\trole: \"user\";\n\tcontent: string | (TextContent | ImageContent)[];\n\ttimestamp: number; // Unix timestamp in milliseconds\n}\n\nexport interface AssistantMessage {\n\trole: \"assistant\";\n\tcontent: (TextContent | ThinkingContent | ToolCall)[];\n\tapi: Api;\n\tprovider: ProviderId;\n\tmodel: string;\n\tresponseModel?: string; // Concrete `chunk.model` when different from the requested `model` (e.g. OpenRouter `auto` -> `anthropic/...`)\n\tresponseId?: string; // Provider-specific response/message identifier when the upstream API exposes one\n\tdiagnostics?: AssistantMessageDiagnostic[]; // Redacted provider/runtime diagnostics for failures and recoveries.\n\tusage: Usage;\n\tstopReason: StopReason;\n\terrorMessage?: string;\n\ttimestamp: number; // Unix timestamp in milliseconds\n}\n\nexport interface ToolResultMessage<TDetails = any> {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: (TextContent | ImageContent)[]; // Supports text and images\n\tdetails?: TDetails;\n\tisError: boolean;\n\ttimestamp: number; // Unix timestamp in milliseconds\n}\n\nexport type Message = UserMessage | AssistantMessage | ToolResultMessage;\n\nexport type ImagesInputContent = TextContent | ImageContent;\nexport type ImagesOutputContent = TextContent | ImageContent;\n\nexport interface ImagesContext {\n\tinput: ImagesInputContent[];\n}\n\nexport type ImagesStopReason = \"stop\" | \"error\" | \"aborted\";\n\nexport interface AssistantImages {\n\tapi: ImagesApi;\n\tprovider: ImagesProviderId;\n\tmodel: string;\n\toutput: ImagesOutputContent[];\n\tresponseId?: string;\n\tusage?: Usage;\n\tstopReason: ImagesStopReason;\n\terrorMessage?: string;\n\ttimestamp: number; // Unix timestamp in milliseconds\n}\n\nimport type { TSchema } from \"typebox\";\n\nexport interface Tool<TParameters extends TSchema = TSchema> {\n\tname: string;\n\tdescription: string;\n\tparameters: TParameters;\n}\n\nexport interface Context {\n\tsystemPrompt?: string;\n\tmessages: Message[];\n\ttools?: Tool[];\n}\n\n/**\n * Event protocol for AssistantMessageEventStream.\n *\n * Streams should emit `start` before partial updates, then terminate with either:\n * - `done` carrying the final successful AssistantMessage, or\n * - `error` carrying the final AssistantMessage with stopReason \"error\" or \"aborted\"\n * and errorMessage.\n */\nexport type AssistantMessageEvent =\n\t| { type: \"start\"; partial: AssistantMessage }\n\t| { type: \"text_start\"; contentIndex: number; partial: AssistantMessage }\n\t| { type: \"text_delta\"; contentIndex: number; delta: string; partial: AssistantMessage }\n\t| { type: \"text_end\"; contentIndex: number; content: string; partial: AssistantMessage }\n\t| { type: \"thinking_start\"; contentIndex: number; partial: AssistantMessage }\n\t| { type: \"thinking_delta\"; contentIndex: number; delta: string; partial: AssistantMessage }\n\t| { type: \"thinking_end\"; contentIndex: number; content: string; partial: AssistantMessage }\n\t| { type: \"toolcall_start\"; contentIndex: number; partial: AssistantMessage }\n\t| { type: \"toolcall_delta\"; contentIndex: number; delta: string; partial: AssistantMessage }\n\t| { type: \"toolcall_end\"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage }\n\t| { type: \"done\"; reason: Extract<StopReason, \"stop\" | \"length\" | \"toolUse\">; message: AssistantMessage }\n\t| { type: \"error\"; reason: Extract<StopReason, \"aborted\" | \"error\">; error: AssistantMessage };\n\n/**\n * Compatibility settings for OpenAI-compatible completions APIs.\n * Use this to override URL-based auto-detection for custom providers.\n */\nexport interface OpenAICompletionsCompat {\n\t/** Whether the provider supports the `store` field. Default: auto-detected from URL. */\n\tsupportsStore?: boolean;\n\t/** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */\n\tsupportsDeveloperRole?: boolean;\n\t/** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */\n\tsupportsReasoningEffort?: boolean;\n\t/** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */\n\tsupportsUsageInStreaming?: boolean;\n\t/** Which field to use for max tokens. Default: auto-detected from URL. */\n\tmaxTokensField?: \"max_completion_tokens\" | \"max_tokens\";\n\t/** Whether tool results require the `name` field. Default: auto-detected from URL. */\n\trequiresToolResultName?: boolean;\n\t/** Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. */\n\trequiresAssistantAfterToolResult?: boolean;\n\t/** Whether thinking blocks must be converted to text blocks with <thinking> delimiters. Default: auto-detected from URL. */\n\trequiresThinkingAsText?: boolean;\n\t/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */\n\trequiresReasoningContentOnAssistantMessages?: boolean;\n\t/** Format for reasoning/thinking parameter. \"openai\" uses reasoning_effort, \"openrouter\" uses reasoning: { effort }, \"deepseek\" uses thinking: { type } plus reasoning_effort when supported, \"together\" uses reasoning: { enabled } plus reasoning_effort when supported, \"zai\" uses thinking: { type }, \"qwen\" uses top-level enable_thinking: boolean, \"qwen-chat-template\" uses chat_template_kwargs.enable_thinking and preserve_thinking, \"chat-template\" uses configurable chat_template_kwargs, \"string-thinking\" uses top-level thinking: string, and \"ant-ling\" uses reasoning: { effort } only when the mapped effort is non-null. Default: \"openai\". */\n\tthinkingFormat?:\n\t\t| \"openai\"\n\t\t| \"openrouter\"\n\t\t| \"deepseek\"\n\t\t| \"together\"\n\t\t| \"zai\"\n\t\t| \"qwen\"\n\t\t| \"chat-template\"\n\t\t| \"qwen-chat-template\"\n\t\t| \"string-thinking\"\n\t\t| \"ant-ling\";\n\t/** Kwargs to send as `chat_template_kwargs` when `thinkingFormat` is `chat-template`. Use `{ \"$var\": \"thinking.enabled\" }` or `{ \"$var\": \"thinking.effort\" }` for pi-controlled thinking values. */\n\tchatTemplateKwargs?: Record<string, ChatTemplateKwargValue>;\n\t/** OpenRouter-compatible routing preferences sent as the `provider` request field. */\n\topenRouterRouting?: OpenRouterRouting;\n\t/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */\n\tvercelGatewayRouting?: VercelGatewayRouting;\n\t/** Whether z.ai supports top-level `tool_stream: true` for streaming tool call deltas. Default: false. */\n\tzaiToolStream?: boolean;\n\t/** Whether the provider supports the `strict` field in tool definitions. Default: true. */\n\tsupportsStrictMode?: boolean;\n\t/** Cache control convention for prompt caching. \"anthropic\" applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content. */\n\tcacheControlFormat?: \"anthropic\";\n\t/** Whether to send known session-affinity headers (`session_id`, `x-client-request-id`, `x-session-affinity`) from `options.sessionId` when caching is enabled. Default: false. */\n\tsendSessionAffinityHeaders?: boolean;\n\t/** Whether the provider supports long prompt cache retention (`prompt_cache_retention: \"24h\"` or Anthropic-style `cache_control.ttl: \"1h\"`, depending on format). Default: true. */\n\tsupportsLongCacheRetention?: boolean;\n}\n\n/** Compatibility settings for OpenAI Responses APIs. */\nexport interface OpenAIResponsesCompat {\n\t/** Whether the provider supports the `developer` role (vs `system`). Default: true. */\n\tsupportsDeveloperRole?: boolean;\n\t/** Whether to send the OpenAI `session_id` cache-affinity header from `options.sessionId` when caching is enabled. Default: true. */\n\tsendSessionIdHeader?: boolean;\n\t/** Whether the provider supports `prompt_cache_retention: \"24h\"`. Default: true. */\n\tsupportsLongCacheRetention?: boolean;\n}\n\n/** Compatibility settings for Anthropic Messages-compatible APIs. */\nexport interface AnthropicMessagesCompat {\n\t/**\n\t * Whether the provider accepts per-tool `eager_input_streaming`.\n\t * When false, the Anthropic provider omits `tools[].eager_input_streaming`\n\t * and sends the legacy `fine-grained-tool-streaming-2025-05-14` beta header\n\t * for tool-enabled requests.\n\t * Default: true.\n\t */\n\tsupportsEagerToolInputStreaming?: boolean;\n\t/** Whether the provider supports Anthropic long cache retention (`cache_control.ttl: \"1h\"`). Default: true. */\n\tsupportsLongCacheRetention?: boolean;\n\t/**\n\t * Whether to send the `x-session-affinity` header from `options.sessionId`\n\t * when caching is enabled. Required for providers like Fireworks that use\n\t * session affinity for prompt cache routing (requests to the same replica\n\t * maximize cache hits).\n\t * Default: false.\n\t */\n\tsendSessionAffinityHeaders?: boolean;\n\t/**\n\t * Whether the provider supports Anthropic-style `cache_control` markers on\n\t * tool definitions. When false, `cache_control` is omitted from tool params.\n\t * Some Anthropic-compatible providers (e.g., Fireworks) do not support this\n\t * field on tools and may reject or ignore it.\n\t * Default: true.\n\t */\n\tsupportsCacheControlOnTools?: boolean;\n\t/**\n\t * Whether the model accepts the Anthropic `temperature` request field.\n\t * Claude Opus 4.7+ rejects non-default temperature values.\n\t * Default: true.\n\t */\n\tsupportsTemperature?: boolean;\n\t/**\n\t * Whether to force adaptive thinking (`thinking.type: \"adaptive\"` plus\n\t * `output_config.effort`) regardless of the model id. Built-in models that\n\t * require adaptive thinking set this in generated metadata. Custom\n\t * Anthropic-compatible providers can set this to `true` for any model whose\n\t * upstream requires the adaptive format. Set to `false` to\n\t * opt out on overridden built-in models.\n\t * Default: false.\n\t */\n\tforceAdaptiveThinking?: boolean;\n\t/** Whether to replay empty thinking signatures as `signature: \"\"` instead of converting thinking to text. Default: false. */\n\tallowEmptySignature?: boolean;\n}\n\n/**\n * OpenRouter provider routing preferences.\n * Controls which upstream providers OpenRouter routes requests to.\n * Sent as the `provider` field in the OpenRouter API request body.\n * @see https://openrouter.ai/docs/guides/routing/provider-selection\n */\nexport interface OpenRouterRouting {\n\t/** Whether to allow backup providers to serve requests. Default: true. */\n\tallow_fallbacks?: boolean;\n\t/** Whether to filter providers to only those that support all parameters in the request. Default: false. */\n\trequire_parameters?: boolean;\n\t/** Data collection setting. \"allow\" (default): allow providers that may store/train on data. \"deny\": only use providers that don't collect user data. */\n\tdata_collection?: \"deny\" | \"allow\";\n\t/** Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. */\n\tzdr?: boolean;\n\t/** Whether to restrict routing to only models that allow text distillation. */\n\tenforce_distillable_text?: boolean;\n\t/** An ordered list of provider names/slugs to try in sequence, falling back to the next if unavailable. */\n\torder?: string[];\n\t/** List of provider names/slugs to exclusively allow for this request. */\n\tonly?: string[];\n\t/** List of provider names/slugs to skip for this request. */\n\tignore?: string[];\n\t/** A list of quantization levels to filter providers by (e.g., [\"fp16\", \"bf16\", \"fp8\", \"fp6\", \"int8\", \"int4\", \"fp4\", \"fp32\"]). */\n\tquantizations?: string[];\n\t/** Sorting strategy. Can be a string (e.g., \"price\", \"throughput\", \"latency\") or an object with `by` and `partition`. */\n\tsort?:\n\t\t| string\n\t\t| {\n\t\t\t\t/** The sorting metric: \"price\", \"throughput\", \"latency\". */\n\t\t\t\tby?: string;\n\t\t\t\t/** Partitioning strategy: \"model\" (default) or \"none\". */\n\t\t\t\tpartition?: string | null;\n\t\t };\n\t/** Maximum price per million tokens (USD). */\n\tmax_price?: {\n\t\t/** Price per million prompt tokens. */\n\t\tprompt?: number | string;\n\t\t/** Price per million completion tokens. */\n\t\tcompletion?: number | string;\n\t\t/** Price per image. */\n\t\timage?: number | string;\n\t\t/** Price per audio unit. */\n\t\taudio?: number | string;\n\t\t/** Price per request. */\n\t\trequest?: number | string;\n\t};\n\t/** Preferred minimum throughput (tokens/second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. */\n\tpreferred_min_throughput?:\n\t\t| number\n\t\t| {\n\t\t\t\t/** Minimum tokens/second at the 50th percentile. */\n\t\t\t\tp50?: number;\n\t\t\t\t/** Minimum tokens/second at the 75th percentile. */\n\t\t\t\tp75?: number;\n\t\t\t\t/** Minimum tokens/second at the 90th percentile. */\n\t\t\t\tp90?: number;\n\t\t\t\t/** Minimum tokens/second at the 99th percentile. */\n\t\t\t\tp99?: number;\n\t\t };\n\t/** Preferred maximum latency (seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. */\n\tpreferred_max_latency?:\n\t\t| number\n\t\t| {\n\t\t\t\t/** Maximum latency in seconds at the 50th percentile. */\n\t\t\t\tp50?: number;\n\t\t\t\t/** Maximum latency in seconds at the 75th percentile. */\n\t\t\t\tp75?: number;\n\t\t\t\t/** Maximum latency in seconds at the 90th percentile. */\n\t\t\t\tp90?: number;\n\t\t\t\t/** Maximum latency in seconds at the 99th percentile. */\n\t\t\t\tp99?: number;\n\t\t };\n}\n\n/**\n * Vercel AI Gateway routing preferences.\n * Controls which upstream providers the gateway routes requests to.\n * @see https://vercel.com/docs/ai-gateway/models-and-providers/provider-options\n */\nexport interface VercelGatewayRouting {\n\t/** List of provider slugs to exclusively use for this request (e.g., [\"bedrock\", \"anthropic\"]). */\n\tonly?: string[];\n\t/** List of provider slugs to try in order (e.g., [\"anthropic\", \"openai\"]). */\n\torder?: string[];\n}\n\nexport interface ModelCostRates {\n\tinput: number; // $/million tokens\n\toutput: number; // $/million tokens\n\tcacheRead: number; // $/million tokens\n\tcacheWrite: number; // $/million tokens\n}\n\nexport interface ModelCostTier extends ModelCostRates {\n\t/** Use this tier for requests whose total input usage exceeds this token count. */\n\tinputTokensAbove: number;\n}\n\nexport interface ModelCost extends ModelCostRates {\n\t/** Request-wide pricing tiers. The highest matching input threshold applies to the full request. */\n\ttiers?: ModelCostTier[];\n}\n\n// Model interface for the unified model system\nexport interface Model<TApi extends Api> {\n\tid: string;\n\tname: string;\n\tapi: TApi;\n\tprovider: ProviderId;\n\tbaseUrl: string;\n\treasoning: boolean;\n\t/**\n\t * Maps pi thinking levels to provider/model-specific values.\n\t * Missing keys use provider defaults. null marks a level as unsupported.\n\t */\n\tthinkingLevelMap?: ThinkingLevelMap;\n\tinput: (\"text\" | \"image\")[];\n\tcost: ModelCost;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\theaders?: Record<string, string>;\n\t/** Compatibility overrides for OpenAI-compatible APIs. If not set, auto-detected from baseUrl. */\n\tcompat?: TApi extends \"openai-completions\"\n\t\t? OpenAICompletionsCompat\n\t\t: TApi extends \"openai-responses\"\n\t\t\t? OpenAIResponsesCompat\n\t\t\t: TApi extends \"anthropic-messages\"\n\t\t\t\t? AnthropicMessagesCompat\n\t\t\t\t: never;\n}\n\nexport interface ImagesModel<TApi extends ImagesApi>\n\textends Omit<Model<Api>, \"api\" | \"provider\" | \"reasoning\" | \"contextWindow\" | \"maxTokens\" | \"compat\"> {\n\tapi: TApi;\n\tprovider: ImagesProviderId;\n\toutput: (\"text\" | \"image\")[];\n}\n"]}
|
package/dist/utils/estimate.d.ts
CHANGED
|
@@ -2,11 +2,11 @@ import type { Context, ImageContent, Message, TextContent, Usage } from "../type
|
|
|
2
2
|
export interface ContextUsageEstimate {
|
|
3
3
|
/** Estimated total context tokens. */
|
|
4
4
|
tokens: number;
|
|
5
|
-
/** Tokens reported by the most recent assistant usage block. */
|
|
5
|
+
/** Tokens reported by the most recent applicable assistant usage block. */
|
|
6
6
|
usageTokens: number;
|
|
7
|
-
/** Estimated tokens after the most recent assistant usage block. */
|
|
7
|
+
/** Estimated tokens after the most recent applicable assistant usage block. */
|
|
8
8
|
trailingTokens: number;
|
|
9
|
-
/** Index of the message that provided usage, or null when none exists. */
|
|
9
|
+
/** Index of the applicable message that provided usage, or null when none exists. */
|
|
10
10
|
lastUsageIndex: number | null;
|
|
11
11
|
}
|
|
12
12
|
export declare function calculateContextTokens(usage: Usage): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"estimate.d.ts","sourceRoot":"","sources":["../../src/utils/estimate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAExG,MAAM,WAAW,oBAAoB;IACpC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,
|
|
1
|
+
{"version":3,"file":"estimate.d.ts","sourceRoot":"","sources":["../../src/utils/estimate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAExG,MAAM,WAAW,oBAAoB;IACpC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,qFAAqF;IACrF,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAKD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE3D;AAkBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,GAAG,YAAY,CAAC,GAAG,MAAM,CAE7G;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAgB9D;AAgDD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,OAAO,EAAE,GAAG,oBAAoB,CAiBjG","sourcesContent":["import type { AssistantMessage, Context, ImageContent, Message, TextContent, Usage } from \"../types.ts\";\n\nexport interface ContextUsageEstimate {\n\t/** Estimated total context tokens. */\n\ttokens: number;\n\t/** Tokens reported by the most recent applicable assistant usage block. */\n\tusageTokens: number;\n\t/** Estimated tokens after the most recent applicable assistant usage block. */\n\ttrailingTokens: number;\n\t/** Index of the applicable message that provided usage, or null when none exists. */\n\tlastUsageIndex: number | null;\n}\n\nconst CHARS_PER_TOKEN = 4;\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\nfunction safeJsonStringify(value: unknown): string {\n\ttry {\n\t\treturn JSON.stringify(value) ?? \"undefined\";\n\t} catch {\n\t\treturn \"[unserializable]\";\n\t}\n}\n\nfunction estimateTextAndImageContentChars(content: string | Array<TextContent | ImageContent>): number {\n\tif (typeof content === \"string\") return content.length;\n\n\tlet chars = 0;\n\tfor (const block of content) chars += block.type === \"text\" ? block.text.length : ESTIMATED_IMAGE_CHARS;\n\treturn chars;\n}\n\nexport function estimateTextTokens(text: string): number {\n\treturn Math.ceil(text.length / CHARS_PER_TOKEN);\n}\n\nexport function estimateTextAndImageContentTokens(content: string | Array<TextContent | ImageContent>): number {\n\treturn Math.ceil(estimateTextAndImageContentChars(content) / CHARS_PER_TOKEN);\n}\n\nexport function estimateMessageTokens(message: Message): number {\n\tlet chars = 0;\n\n\tif (message.role === \"user\") return estimateTextAndImageContentTokens(message.content);\n\tif (message.role === \"toolResult\") return estimateTextAndImageContentTokens(message.content);\n\n\tfor (const block of message.content) {\n\t\tif (block.type === \"text\") {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"thinking\") {\n\t\t\tchars += block.thinking.length;\n\t\t} else {\n\t\t\tchars += block.name.length + safeJsonStringify(block.arguments).length;\n\t\t}\n\t}\n\treturn Math.ceil(chars / CHARS_PER_TOKEN);\n}\n\nfunction getLastAssistantUsageInfo(messages: readonly Message[]): { usage: Usage; index: number } | undefined {\n\tlet latestPrefixTimestamp = Number.NEGATIVE_INFINITY;\n\tlet usageInfo: { usage: Usage; index: number } | undefined;\n\n\tfor (let i = 0; i < messages.length; i++) {\n\t\tconst message = messages[i];\n\t\tif (message.role === \"assistant\") {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\t// A newer prefix message was inserted after this response (for example, a\n\t\t\t// compaction summary), so its usage cannot describe the current prefix.\n\t\t\tconst usageAppliesToPrefix = assistant.timestamp >= latestPrefixTimestamp;\n\t\t\tif (\n\t\t\t\tusageAppliesToPrefix &&\n\t\t\t\tassistant.stopReason !== \"aborted\" &&\n\t\t\t\tassistant.stopReason !== \"error\" &&\n\t\t\t\tcalculateContextTokens(assistant.usage) > 0\n\t\t\t) {\n\t\t\t\tusageInfo = { usage: assistant.usage, index: i };\n\t\t\t}\n\t\t}\n\t\tlatestPrefixTimestamp = Math.max(latestPrefixTimestamp, message.timestamp);\n\t}\n\n\treturn usageInfo;\n}\n\nfunction estimateMessages(messages: readonly Message[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\tif (usageInfo) {\n\t\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\t\tlet trailingTokens = 0;\n\t\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\t\ttrailingTokens += estimateMessageTokens(messages[i]);\n\t\t}\n\t\treturn { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens, lastUsageIndex: usageInfo.index };\n\t}\n\n\tlet tokens = 0;\n\tfor (const message of messages) tokens += estimateMessageTokens(message);\n\treturn { tokens, usageTokens: 0, trailingTokens: tokens, lastUsageIndex: null };\n}\n\nfunction isMessageArray(value: Context | readonly Message[]): value is readonly Message[] {\n\treturn Array.isArray(value);\n}\n\nexport function estimateContextTokens(context: Context | readonly Message[]): ContextUsageEstimate {\n\tif (isMessageArray(context)) return estimateMessages(context);\n\n\tconst estimate = estimateMessages(context.messages);\n\tif (estimate.lastUsageIndex !== null) return estimate;\n\n\tlet prefixTokens = context.systemPrompt ? estimateTextTokens(context.systemPrompt) : 0;\n\tif (context.tools && context.tools.length > 0) {\n\t\tprefixTokens += estimateTextTokens(safeJsonStringify(context.tools));\n\t}\n\n\treturn {\n\t\ttokens: estimate.tokens + prefixTokens,\n\t\tusageTokens: estimate.usageTokens,\n\t\ttrailingTokens: estimate.trailingTokens + prefixTokens,\n\t\tlastUsageIndex: estimate.lastUsageIndex,\n\t};\n}\n"]}
|
package/dist/utils/estimate.js
CHANGED
|
@@ -45,17 +45,25 @@ export function estimateMessageTokens(message) {
|
|
|
45
45
|
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
46
46
|
}
|
|
47
47
|
function getLastAssistantUsageInfo(messages) {
|
|
48
|
-
|
|
48
|
+
let latestPrefixTimestamp = Number.NEGATIVE_INFINITY;
|
|
49
|
+
let usageInfo;
|
|
50
|
+
for (let i = 0; i < messages.length; i++) {
|
|
49
51
|
const message = messages[i];
|
|
50
|
-
if (message.role
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
if (message.role === "assistant") {
|
|
53
|
+
const assistant = message;
|
|
54
|
+
// A newer prefix message was inserted after this response (for example, a
|
|
55
|
+
// compaction summary), so its usage cannot describe the current prefix.
|
|
56
|
+
const usageAppliesToPrefix = assistant.timestamp >= latestPrefixTimestamp;
|
|
57
|
+
if (usageAppliesToPrefix &&
|
|
58
|
+
assistant.stopReason !== "aborted" &&
|
|
59
|
+
assistant.stopReason !== "error" &&
|
|
60
|
+
calculateContextTokens(assistant.usage) > 0) {
|
|
61
|
+
usageInfo = { usage: assistant.usage, index: i };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
latestPrefixTimestamp = Math.max(latestPrefixTimestamp, message.timestamp);
|
|
57
65
|
}
|
|
58
|
-
return
|
|
66
|
+
return usageInfo;
|
|
59
67
|
}
|
|
60
68
|
function estimateMessages(messages) {
|
|
61
69
|
const usageInfo = getLastAssistantUsageInfo(messages);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"estimate.js","sourceRoot":"","sources":["../../src/utils/estimate.ts"],"names":[],"mappings":"AAaA,MAAM,eAAe,GAAG,CAAC,CAAC;AAC1B,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC,MAAM,UAAU,sBAAsB,CAAC,KAAY,EAAU;IAC5D,OAAO,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,UAAU,CAAC;AAAA,CAC5F;AAED,SAAS,iBAAiB,CAAC,KAAc,EAAU;IAClD,IAAI,CAAC;QACJ,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,kBAAkB,CAAC;IAC3B,CAAC;AAAA,CACD;AAED,SAAS,gCAAgC,CAAC,OAAmD,EAAU;IACtG,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC;IAEvD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,KAAK,IAAI,OAAO;QAAE,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC;IACxG,OAAO,KAAK,CAAC;AAAA,CACb;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAU;IACxD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC;AAAA,CAChD;AAED,MAAM,UAAU,iCAAiC,CAAC,OAAmD,EAAU;IAC9G,OAAO,IAAI,CAAC,IAAI,CAAC,gCAAgC,CAAC,OAAO,CAAC,GAAG,eAAe,CAAC,CAAC;AAAA,CAC9E;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAgB,EAAU;IAC/D,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,iCAAiC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACvF,IAAI,OAAO,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,iCAAiC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7F,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC3B,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;QAC5B,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACtC,KAAK,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;QAChC,CAAC;aAAM,CAAC;YACP,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC;QACxE,CAAC;IACF,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,eAAe,CAAC,CAAC;AAAA,CAC1C;AAED,SAAS,yBAAyB,CAAC,QAA4B,EAA+C;IAC7G,
|
|
1
|
+
{"version":3,"file":"estimate.js","sourceRoot":"","sources":["../../src/utils/estimate.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage, Context, ImageContent, Message, TextContent, Usage } from \"../types.ts\";\n\nexport interface ContextUsageEstimate {\n\t/** Estimated total context tokens. */\n\ttokens: number;\n\t/** Tokens reported by the most recent applicable assistant usage block. */\n\tusageTokens: number;\n\t/** Estimated tokens after the most recent applicable assistant usage block. */\n\ttrailingTokens: number;\n\t/** Index of the applicable message that provided usage, or null when none exists. */\n\tlastUsageIndex: number | null;\n}\n\nconst CHARS_PER_TOKEN = 4;\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\nfunction safeJsonStringify(value: unknown): string {\n\ttry {\n\t\treturn JSON.stringify(value) ?? \"undefined\";\n\t} catch {\n\t\treturn \"[unserializable]\";\n\t}\n}\n\nfunction estimateTextAndImageContentChars(content: string | Array<TextContent | ImageContent>): number {\n\tif (typeof content === \"string\") return content.length;\n\n\tlet chars = 0;\n\tfor (const block of content) chars += block.type === \"text\" ? block.text.length : ESTIMATED_IMAGE_CHARS;\n\treturn chars;\n}\n\nexport function estimateTextTokens(text: string): number {\n\treturn Math.ceil(text.length / CHARS_PER_TOKEN);\n}\n\nexport function estimateTextAndImageContentTokens(content: string | Array<TextContent | ImageContent>): number {\n\treturn Math.ceil(estimateTextAndImageContentChars(content) / CHARS_PER_TOKEN);\n}\n\nexport function estimateMessageTokens(message: Message): number {\n\tlet chars = 0;\n\n\tif (message.role === \"user\") return estimateTextAndImageContentTokens(message.content);\n\tif (message.role === \"toolResult\") return estimateTextAndImageContentTokens(message.content);\n\n\tfor (const block of message.content) {\n\t\tif (block.type === \"text\") {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"thinking\") {\n\t\t\tchars += block.thinking.length;\n\t\t} else {\n\t\t\tchars += block.name.length + safeJsonStringify(block.arguments).length;\n\t\t}\n\t}\n\treturn Math.ceil(chars / CHARS_PER_TOKEN);\n}\n\nfunction getLastAssistantUsageInfo(messages: readonly Message[]): { usage: Usage; index: number } | undefined {\n\tlet latestPrefixTimestamp = Number.NEGATIVE_INFINITY;\n\tlet usageInfo: { usage: Usage; index: number } | undefined;\n\n\tfor (let i = 0; i < messages.length; i++) {\n\t\tconst message = messages[i];\n\t\tif (message.role === \"assistant\") {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\t// A newer prefix message was inserted after this response (for example, a\n\t\t\t// compaction summary), so its usage cannot describe the current prefix.\n\t\t\tconst usageAppliesToPrefix = assistant.timestamp >= latestPrefixTimestamp;\n\t\t\tif (\n\t\t\t\tusageAppliesToPrefix &&\n\t\t\t\tassistant.stopReason !== \"aborted\" &&\n\t\t\t\tassistant.stopReason !== \"error\" &&\n\t\t\t\tcalculateContextTokens(assistant.usage) > 0\n\t\t\t) {\n\t\t\t\tusageInfo = { usage: assistant.usage, index: i };\n\t\t\t}\n\t\t}\n\t\tlatestPrefixTimestamp = Math.max(latestPrefixTimestamp, message.timestamp);\n\t}\n\n\treturn usageInfo;\n}\n\nfunction estimateMessages(messages: readonly Message[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\tif (usageInfo) {\n\t\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\t\tlet trailingTokens = 0;\n\t\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\t\ttrailingTokens += estimateMessageTokens(messages[i]);\n\t\t}\n\t\treturn { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens, lastUsageIndex: usageInfo.index };\n\t}\n\n\tlet tokens = 0;\n\tfor (const message of messages) tokens += estimateMessageTokens(message);\n\treturn { tokens, usageTokens: 0, trailingTokens: tokens, lastUsageIndex: null };\n}\n\nfunction isMessageArray(value: Context | readonly Message[]): value is readonly Message[] {\n\treturn Array.isArray(value);\n}\n\nexport function estimateContextTokens(context: Context | readonly Message[]): ContextUsageEstimate {\n\tif (isMessageArray(context)) return estimateMessages(context);\n\n\tconst estimate = estimateMessages(context.messages);\n\tif (estimate.lastUsageIndex !== null) return estimate;\n\n\tlet prefixTokens = context.systemPrompt ? estimateTextTokens(context.systemPrompt) : 0;\n\tif (context.tools && context.tools.length > 0) {\n\t\tprefixTokens += estimateTextTokens(safeJsonStringify(context.tools));\n\t}\n\n\treturn {\n\t\ttokens: estimate.tokens + prefixTokens,\n\t\tusageTokens: estimate.usageTokens,\n\t\ttrailingTokens: estimate.trailingTokens + prefixTokens,\n\t\tlastUsageIndex: estimate.lastUsageIndex,\n\t};\n}\n"]}
|
|
@@ -2,6 +2,7 @@ type OAuthDeviceCodeIncompletePollResult = {
|
|
|
2
2
|
status: "pending";
|
|
3
3
|
} | {
|
|
4
4
|
status: "slow_down";
|
|
5
|
+
intervalSeconds?: number;
|
|
5
6
|
} | {
|
|
6
7
|
status: "failed";
|
|
7
8
|
message: string;
|
|
@@ -13,6 +14,7 @@ export type OAuthDeviceCodePollResult<T> = OAuthDeviceCodeIncompletePollResult |
|
|
|
13
14
|
export type OAuthDeviceCodePollOptions<T> = {
|
|
14
15
|
intervalSeconds?: number;
|
|
15
16
|
expiresInSeconds?: number;
|
|
17
|
+
waitBeforeFirstPoll?: boolean;
|
|
16
18
|
poll: () => Promise<OAuthDeviceCodePollResult<T>>;
|
|
17
19
|
signal?: AbortSignal;
|
|
18
20
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-code.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/device-code.ts"],"names":[],"mappings":"AAUA,KAAK,mCAAmC,GACrC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"device-code.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/device-code.ts"],"names":[],"mappings":"AAUA,KAAK,mCAAmC,GACrC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI,mCAAmC,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAElH,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,IAAI,EAAE,MAAM,OAAO,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAsBF,wBAAsB,uBAAuB,CAAC,CAAC,EAAE,OAAO,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAoDnG","sourcesContent":["const CANCEL_MESSAGE = \"Login cancelled\";\nconst TIMEOUT_MESSAGE = \"Device flow timed out\";\nconst SLOW_DOWN_TIMEOUT_MESSAGE =\n\t\"Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.\";\nconst MINIMUM_INTERVAL_MS = 1000;\n// RFC 8628 section 3.2: if the authorization server omits `interval`, the client must use 5 seconds.\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\n// RFC 8628 section 3.5: `slow_down` means the polling interval must increase by 5 seconds.\nconst SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000;\n\ntype OAuthDeviceCodeIncompletePollResult =\n\t| { status: \"pending\" }\n\t| { status: \"slow_down\"; intervalSeconds?: number }\n\t| { status: \"failed\"; message: string };\n\nexport type OAuthDeviceCodePollResult<T> = OAuthDeviceCodeIncompletePollResult | { status: \"complete\"; value: T };\n\nexport type OAuthDeviceCodePollOptions<T> = {\n\tintervalSeconds?: number;\n\texpiresInSeconds?: number;\n\twaitBeforeFirstPoll?: boolean;\n\tpoll: () => Promise<OAuthDeviceCodePollResult<T>>;\n\tsignal?: AbortSignal;\n};\n\nfunction abortableSleep(ms: number, signal: AbortSignal | undefined, cancelMessage: string): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(cancelMessage));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(cancelMessage));\n\t\t};\n\t\tconst timeout = setTimeout(() => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\nexport async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodePollOptions<T>): Promise<T> {\n\tconst deadline =\n\t\ttypeof options.expiresInSeconds === \"number\"\n\t\t\t? Date.now() + options.expiresInSeconds * 1000\n\t\t\t: Number.POSITIVE_INFINITY;\n\tlet intervalMs = Math.max(\n\t\tMINIMUM_INTERVAL_MS,\n\t\tMath.floor((options.intervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000),\n\t);\n\n\tlet slowDownResponses = 0;\n\tif (options.waitBeforeFirstPoll) {\n\t\tconst remainingMs = deadline - Date.now();\n\t\tif (remainingMs > 0) {\n\t\t\tawait abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);\n\t\t}\n\t}\n\n\twhile (Date.now() < deadline) {\n\t\tif (options.signal?.aborted) {\n\t\t\tthrow new Error(CANCEL_MESSAGE);\n\t\t}\n\n\t\tconst result = await options.poll();\n\t\tif (result.status === \"complete\") {\n\t\t\treturn result.value;\n\t\t}\n\t\tif (result.status === \"failed\") {\n\t\t\tthrow new Error(result.message);\n\t\t}\n\t\tif (result.status === \"slow_down\") {\n\t\t\tslowDownResponses += 1;\n\t\t\t// Use the server-provided interval when given (GitHub reports the new required minimum\n\t\t\t// in `interval`); trusting only a client-tracked value risks polling early forever under\n\t\t\t// WSL/VM clock drift. Otherwise apply RFC 8628 section 3.5: increase by 5 seconds.\n\t\t\tintervalMs =\n\t\t\t\ttypeof result.intervalSeconds === \"number\" &&\n\t\t\t\tNumber.isFinite(result.intervalSeconds) &&\n\t\t\t\tresult.intervalSeconds > 0\n\t\t\t\t\t? Math.max(MINIMUM_INTERVAL_MS, Math.floor(result.intervalSeconds * 1000))\n\t\t\t\t\t: Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);\n\t\t}\n\n\t\tconst remainingMs = deadline - Date.now();\n\t\tif (remainingMs <= 0) {\n\t\t\tbreak;\n\t\t}\n\n\t\tawait abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);\n\t}\n\n\tthrow new Error(slowDownResponses > 0 ? SLOW_DOWN_TIMEOUT_MESSAGE : TIMEOUT_MESSAGE);\n}\n"]}
|
|
@@ -29,6 +29,12 @@ export async function pollOAuthDeviceCodeFlow(options) {
|
|
|
29
29
|
: Number.POSITIVE_INFINITY;
|
|
30
30
|
let intervalMs = Math.max(MINIMUM_INTERVAL_MS, Math.floor((options.intervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000));
|
|
31
31
|
let slowDownResponses = 0;
|
|
32
|
+
if (options.waitBeforeFirstPoll) {
|
|
33
|
+
const remainingMs = deadline - Date.now();
|
|
34
|
+
if (remainingMs > 0) {
|
|
35
|
+
await abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
32
38
|
while (Date.now() < deadline) {
|
|
33
39
|
if (options.signal?.aborted) {
|
|
34
40
|
throw new Error(CANCEL_MESSAGE);
|
|
@@ -42,8 +48,15 @@ export async function pollOAuthDeviceCodeFlow(options) {
|
|
|
42
48
|
}
|
|
43
49
|
if (result.status === "slow_down") {
|
|
44
50
|
slowDownResponses += 1;
|
|
45
|
-
//
|
|
46
|
-
|
|
51
|
+
// Use the server-provided interval when given (GitHub reports the new required minimum
|
|
52
|
+
// in `interval`); trusting only a client-tracked value risks polling early forever under
|
|
53
|
+
// WSL/VM clock drift. Otherwise apply RFC 8628 section 3.5: increase by 5 seconds.
|
|
54
|
+
intervalMs =
|
|
55
|
+
typeof result.intervalSeconds === "number" &&
|
|
56
|
+
Number.isFinite(result.intervalSeconds) &&
|
|
57
|
+
result.intervalSeconds > 0
|
|
58
|
+
? Math.max(MINIMUM_INTERVAL_MS, Math.floor(result.intervalSeconds * 1000))
|
|
59
|
+
: Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);
|
|
47
60
|
}
|
|
48
61
|
const remainingMs = deadline - Date.now();
|
|
49
62
|
if (remainingMs <= 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-code.js","sourceRoot":"","sources":["../../../src/utils/oauth/device-code.ts"],"names":[],"mappings":"AAAA,MAAM,cAAc,GAAG,iBAAiB,CAAC;AACzC,MAAM,eAAe,GAAG,uBAAuB,CAAC;AAChD,MAAM,yBAAyB,GAC9B,gLAAgL,CAAC;AAClL,MAAM,mBAAmB,GAAG,IAAI,CAAC;AACjC,qGAAqG;AACrG,MAAM,6BAA6B,GAAG,CAAC,CAAC;AACxC,2FAA2F;AAC3F,MAAM,+BAA+B,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"device-code.js","sourceRoot":"","sources":["../../../src/utils/oauth/device-code.ts"],"names":[],"mappings":"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","sourcesContent":["const CANCEL_MESSAGE = \"Login cancelled\";\nconst TIMEOUT_MESSAGE = \"Device flow timed out\";\nconst SLOW_DOWN_TIMEOUT_MESSAGE =\n\t\"Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.\";\nconst MINIMUM_INTERVAL_MS = 1000;\n// RFC 8628 section 3.2: if the authorization server omits `interval`, the client must use 5 seconds.\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\n// RFC 8628 section 3.5: `slow_down` means the polling interval must increase by 5 seconds.\nconst SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000;\n\ntype OAuthDeviceCodeIncompletePollResult =\n\t| { status: \"pending\" }\n\t| { status: \"slow_down\"; intervalSeconds?: number }\n\t| { status: \"failed\"; message: string };\n\nexport type OAuthDeviceCodePollResult<T> = OAuthDeviceCodeIncompletePollResult | { status: \"complete\"; value: T };\n\nexport type OAuthDeviceCodePollOptions<T> = {\n\tintervalSeconds?: number;\n\texpiresInSeconds?: number;\n\twaitBeforeFirstPoll?: boolean;\n\tpoll: () => Promise<OAuthDeviceCodePollResult<T>>;\n\tsignal?: AbortSignal;\n};\n\nfunction abortableSleep(ms: number, signal: AbortSignal | undefined, cancelMessage: string): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(cancelMessage));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(cancelMessage));\n\t\t};\n\t\tconst timeout = setTimeout(() => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\nexport async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodePollOptions<T>): Promise<T> {\n\tconst deadline =\n\t\ttypeof options.expiresInSeconds === \"number\"\n\t\t\t? Date.now() + options.expiresInSeconds * 1000\n\t\t\t: Number.POSITIVE_INFINITY;\n\tlet intervalMs = Math.max(\n\t\tMINIMUM_INTERVAL_MS,\n\t\tMath.floor((options.intervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000),\n\t);\n\n\tlet slowDownResponses = 0;\n\tif (options.waitBeforeFirstPoll) {\n\t\tconst remainingMs = deadline - Date.now();\n\t\tif (remainingMs > 0) {\n\t\t\tawait abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);\n\t\t}\n\t}\n\n\twhile (Date.now() < deadline) {\n\t\tif (options.signal?.aborted) {\n\t\t\tthrow new Error(CANCEL_MESSAGE);\n\t\t}\n\n\t\tconst result = await options.poll();\n\t\tif (result.status === \"complete\") {\n\t\t\treturn result.value;\n\t\t}\n\t\tif (result.status === \"failed\") {\n\t\t\tthrow new Error(result.message);\n\t\t}\n\t\tif (result.status === \"slow_down\") {\n\t\t\tslowDownResponses += 1;\n\t\t\t// Use the server-provided interval when given (GitHub reports the new required minimum\n\t\t\t// in `interval`); trusting only a client-tracked value risks polling early forever under\n\t\t\t// WSL/VM clock drift. Otherwise apply RFC 8628 section 3.5: increase by 5 seconds.\n\t\t\tintervalMs =\n\t\t\t\ttypeof result.intervalSeconds === \"number\" &&\n\t\t\t\tNumber.isFinite(result.intervalSeconds) &&\n\t\t\t\tresult.intervalSeconds > 0\n\t\t\t\t\t? Math.max(MINIMUM_INTERVAL_MS, Math.floor(result.intervalSeconds * 1000))\n\t\t\t\t\t: Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);\n\t\t}\n\n\t\tconst remainingMs = deadline - Date.now();\n\t\tif (remainingMs <= 0) {\n\t\t\tbreak;\n\t\t}\n\n\t\tawait abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);\n\t}\n\n\tthrow new Error(slowDownResponses > 0 ? SLOW_DOWN_TIMEOUT_MESSAGE : TIMEOUT_MESSAGE);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"github-copilot.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/github-copilot.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,qBAAqB,CAAC;AAItE,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAuB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAqCrH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS5D;AA4BD,wBAAgB,uBAAuB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CASzF;AA+LD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,YAAY,EAAE,MAAM,EACpB,gBAAgB,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AA8CD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAClD,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACvG,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuC5B;AAQD,eAAO,MAAM,kBAAkB,EAAE,SA4BhC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,sBAoCxC,CAAC","sourcesContent":["/**\n * GitHub Copilot OAuth flow\n */\n\nimport type { OAuthAuth, OAuthCredential } from \"../../auth/types.ts\";\nimport { GITHUB_COPILOT_MODELS } from \"../../providers/github-copilot.models.ts\";\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport type { OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\ntype CopilotCredentials = OAuthCredentials & {\n\tenterpriseUrl?: string;\n\tavailableModelIds: string[];\n};\n\nconst decode = (s: string) => atob(s);\nconst CLIENT_ID = decode(\"SXYxLmI1MDdhMDhjODdlY2ZlOTg=\");\n\nconst COPILOT_HEADERS = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n} as const;\nconst COPILOT_API_VERSION = \"2026-06-01\";\n\ntype DeviceCodeResponse = {\n\tdevice_code: string;\n\tuser_code: string;\n\tverification_uri: string;\n\tinterval?: number;\n\texpires_in: number;\n};\n\ntype DeviceTokenSuccessResponse = {\n\taccess_token: string;\n\ttoken_type?: string;\n\tscope?: string;\n};\n\ntype DeviceTokenErrorResponse = {\n\terror: string;\n\terror_description?: string;\n};\n\nexport function normalizeDomain(input: string): string | null {\n\tconst trimmed = input.trim();\n\tif (!trimmed) return null;\n\ttry {\n\t\tconst url = trimmed.includes(\"://\") ? new URL(trimmed) : new URL(`https://${trimmed}`);\n\t\treturn url.hostname;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction getUrls(domain: string): {\n\tdeviceCodeUrl: string;\n\taccessTokenUrl: string;\n\tcopilotTokenUrl: string;\n} {\n\treturn {\n\t\tdeviceCodeUrl: `https://${domain}/login/device/code`,\n\t\taccessTokenUrl: `https://${domain}/login/oauth/access_token`,\n\t\tcopilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,\n\t};\n}\n\n/**\n * Parse the proxy-ep from a Copilot token and convert to API base URL.\n * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...\n * Returns API URL like https://api.individual.githubcopilot.com\n */\nfunction getBaseUrlFromToken(token: string): string | null {\n\tconst match = token.match(/proxy-ep=([^;]+)/);\n\tif (!match) return null;\n\tconst proxyHost = match[1];\n\t// Convert proxy.xxx to api.xxx\n\tconst apiHost = proxyHost.replace(/^proxy\\./, \"api.\");\n\treturn `https://${apiHost}`;\n}\n\nexport function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {\n\t// If we have a token, extract the base URL from proxy-ep\n\tif (token) {\n\t\tconst urlFromToken = getBaseUrlFromToken(token);\n\t\tif (urlFromToken) return urlFromToken;\n\t}\n\t// Fallback for enterprise or if token parsing fails\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn \"https://api.individual.githubcopilot.com\";\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction isSelectableCopilotModel(item: Record<string, unknown>): boolean {\n\tconst policy = asRecord(item.policy);\n\tconst capabilities = asRecord(item.capabilities);\n\tconst supports = asRecord(capabilities?.supports);\n\treturn item.model_picker_enabled === true && policy?.state !== \"disabled\" && supports?.tool_calls !== false;\n}\n\nfunction parseAvailableCopilotModelIds(raw: unknown): string[] {\n\tconst data = asRecord(raw)?.data;\n\tif (!Array.isArray(data)) {\n\t\tthrow new Error(\"Invalid Copilot models response\");\n\t}\n\n\tconst ids: string[] = [];\n\tfor (const rawItem of data) {\n\t\tconst item = asRecord(rawItem);\n\t\tconst id = item?.id;\n\t\tif (typeof id === \"string\" && item && isSelectableCopilotModel(item)) {\n\t\t\tids.push(id);\n\t\t}\n\t}\n\treturn ids;\n}\n\nasync function fetchAvailableGitHubCopilotModelIds(copilotToken: string, enterpriseDomain?: string): Promise<string[]> {\n\tconst baseUrl = getGitHubCopilotBaseUrl(copilotToken, enterpriseDomain);\n\tconst raw = await fetchJson(`${baseUrl}/models`, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${copilotToken}`,\n\t\t\t...COPILOT_HEADERS,\n\t\t\t\"X-GitHub-Api-Version\": COPILOT_API_VERSION,\n\t\t},\n\t\tsignal: AbortSignal.timeout(5000),\n\t});\n\treturn parseAvailableCopilotModelIds(raw);\n}\n\nasync function fetchJson(url: string, init: RequestInit): Promise<unknown> {\n\tconst response = await fetch(url, init);\n\tif (!response.ok) {\n\t\tconst text = await response.text();\n\t\tthrow new Error(`${response.status} ${response.statusText}: ${text}`);\n\t}\n\treturn response.json();\n}\n\nasync function startDeviceFlow(domain: string): Promise<DeviceCodeResponse> {\n\tconst urls = getUrls(domain);\n\tconst data = await fetchJson(urls.deviceCodeUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t},\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: \"read:user\",\n\t\t}),\n\t});\n\n\tif (!data || typeof data !== \"object\") {\n\t\tthrow new Error(\"Invalid device code response\");\n\t}\n\n\tconst deviceCode = (data as Record<string, unknown>).device_code;\n\tconst userCode = (data as Record<string, unknown>).user_code;\n\tconst verificationUri = (data as Record<string, unknown>).verification_uri;\n\tconst interval = (data as Record<string, unknown>).interval;\n\tconst expiresIn = (data as Record<string, unknown>).expires_in;\n\n\tif (\n\t\ttypeof deviceCode !== \"string\" ||\n\t\ttypeof userCode !== \"string\" ||\n\t\ttypeof verificationUri !== \"string\" ||\n\t\t(interval !== undefined && typeof interval !== \"number\") ||\n\t\ttypeof expiresIn !== \"number\"\n\t) {\n\t\tthrow new Error(\"Invalid device code response fields\");\n\t}\n\n\t// The verification URI is opened in the user's browser and to prevent `open` from\n\t// opening an executable or similar, we force it to be a URL.\n\tlet parsedUri: URL;\n\ttry {\n\t\tparsedUri = new URL(verificationUri);\n\t} catch {\n\t\tthrow new Error(\"Untrusted verification_uri in device code response\");\n\t}\n\tif (parsedUri.protocol !== \"https:\" && parsedUri.protocol !== \"http:\") {\n\t\tthrow new Error(\"Untrusted verification_uri in device code response\");\n\t}\n\n\treturn {\n\t\tdevice_code: deviceCode,\n\t\tuser_code: userCode,\n\t\tverification_uri: parsedUri.href,\n\t\tinterval,\n\t\texpires_in: expiresIn,\n\t};\n}\n\nasync function pollForGitHubAccessToken(\n\tdomain: string,\n\tdevice: DeviceCodeResponse,\n\tsignal?: AbortSignal,\n): Promise<string> {\n\tconst urls = getUrls(domain);\n\treturn pollOAuthDeviceCodeFlow<string>({\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst raw = await fetchJson(urls.accessTokenUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: device.device_code,\n\t\t\t\t\tgrant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tif (raw && typeof raw === \"object\" && typeof (raw as DeviceTokenSuccessResponse).access_token === \"string\") {\n\t\t\t\treturn { status: \"complete\", value: (raw as DeviceTokenSuccessResponse).access_token };\n\t\t\t}\n\n\t\t\tif (raw && typeof raw === \"object\" && typeof (raw as DeviceTokenErrorResponse).error === \"string\") {\n\t\t\t\tconst { error, error_description: description } = raw as DeviceTokenErrorResponse;\n\t\t\t\tif (error === \"authorization_pending\") {\n\t\t\t\t\treturn { status: \"pending\" };\n\t\t\t\t}\n\n\t\t\t\tif (error === \"slow_down\") {\n\t\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t\t}\n\n\t\t\t\tconst descriptionSuffix = description ? `: ${description}` : \"\";\n\t\t\t\treturn { status: \"failed\", message: `Device flow failed: ${error}${descriptionSuffix}` };\n\t\t\t}\n\n\t\t\treturn { status: \"failed\", message: \"Invalid device token response\" };\n\t\t},\n\t});\n}\n\nasync function refreshGitHubCopilotAccessToken(\n\trefreshToken: string,\n\tenterpriseDomain?: string,\n): Promise<OAuthCredentials> {\n\tconst domain = enterpriseDomain || \"github.com\";\n\tconst urls = getUrls(domain);\n\n\tconst raw = await fetchJson(urls.copilotTokenUrl, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${refreshToken}`,\n\t\t\t...COPILOT_HEADERS,\n\t\t},\n\t});\n\n\tif (!raw || typeof raw !== \"object\") {\n\t\tthrow new Error(\"Invalid Copilot token response\");\n\t}\n\n\tconst token = (raw as Record<string, unknown>).token;\n\tconst expiresAt = (raw as Record<string, unknown>).expires_at;\n\n\tif (typeof token !== \"string\" || typeof expiresAt !== \"number\") {\n\t\tthrow new Error(\"Invalid Copilot token response fields\");\n\t}\n\n\treturn {\n\t\trefresh: refreshToken,\n\t\taccess: token,\n\t\texpires: expiresAt * 1000 - 5 * 60 * 1000,\n\t\tenterpriseUrl: enterpriseDomain,\n\t};\n}\n\n/**\n * Refresh GitHub Copilot token\n */\nexport async function refreshGitHubCopilotToken(\n\trefreshToken: string,\n\tenterpriseDomain?: string,\n): Promise<OAuthCredentials> {\n\tconst credentials = await refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain);\n\treturn {\n\t\t...credentials,\n\t\tavailableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain),\n\t};\n}\n\n/**\n * Enable a model for the user's GitHub Copilot account.\n * This is required for some models (like Claude, Grok) before they can be used.\n */\nasync function enableGitHubCopilotModel(token: string, modelId: string, enterpriseDomain?: string): Promise<boolean> {\n\tconst baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);\n\tconst url = `${baseUrl}/models/${modelId}/policy`;\n\n\ttry {\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${token}`,\n\t\t\t\t...COPILOT_HEADERS,\n\t\t\t\t\"openai-intent\": \"chat-policy\",\n\t\t\t\t\"x-interaction-type\": \"chat-policy\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({ state: \"enabled\" }),\n\t\t});\n\t\treturn response.ok;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Enable all known GitHub Copilot models that may require policy acceptance.\n * Called after successful login to ensure all models are available.\n */\nasync function enableAllGitHubCopilotModels(\n\ttoken: string,\n\tenterpriseDomain?: string,\n\tonProgress?: (model: string, success: boolean) => void,\n): Promise<void> {\n\tconst models = Object.values(GITHUB_COPILOT_MODELS);\n\tawait Promise.all(\n\t\tmodels.map(async (model) => {\n\t\t\tconst success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);\n\t\t\tonProgress?.(model.id, success);\n\t\t}),\n\t);\n}\n\n/**\n * Login with GitHub Copilot OAuth (device code flow)\n *\n * @param options.onDeviceCode - Callback with URL and user code\n * @param options.onPrompt - Callback to prompt user for input\n * @param options.onProgress - Optional progress callback\n * @param options.signal - Optional AbortSignal for cancellation\n */\nexport async function loginGitHubCopilot(options: {\n\tonDeviceCode: (info: OAuthDeviceCodeInfo) => void;\n\tonPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;\n\tonProgress?: (message: string) => void;\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst input = await options.onPrompt({\n\t\tmessage: \"GitHub Enterprise URL/domain (blank for github.com)\",\n\t\tplaceholder: \"company.ghe.com\",\n\t\tallowEmpty: true,\n\t});\n\n\tif (options.signal?.aborted) {\n\t\tthrow new Error(\"Login cancelled\");\n\t}\n\n\tconst trimmed = input.trim();\n\tconst enterpriseDomain = normalizeDomain(input);\n\tif (trimmed && !enterpriseDomain) {\n\t\tthrow new Error(\"Invalid GitHub Enterprise URL/domain\");\n\t}\n\tconst domain = enterpriseDomain || \"github.com\";\n\n\tconst device = await startDeviceFlow(domain);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code,\n\t\tverificationUri: device.verification_uri,\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in,\n\t});\n\n\tconst githubAccessToken = await pollForGitHubAccessToken(domain, device, options.signal);\n\tconst credentials = await refreshGitHubCopilotAccessToken(githubAccessToken, enterpriseDomain ?? undefined);\n\n\t// Enable all models after successful login\n\toptions.onProgress?.(\"Enabling models...\");\n\tawait enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);\n\n\t// Fetch availability after policy enable so newly enabled models are included,\n\t// while unavailable models are still filtered out.\n\treturn {\n\t\t...credentials,\n\t\tavailableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain ?? undefined),\n\t};\n}\n\nfunction copilotEnterpriseDomain(credential: OAuthCredential): string | undefined {\n\tconst enterpriseUrl = credential.enterpriseUrl;\n\tif (typeof enterpriseUrl !== \"string\" || !enterpriseUrl) return undefined;\n\treturn normalizeDomain(enterpriseUrl) ?? undefined;\n}\n\nexport const githubCopilotOAuth: OAuthAuth = {\n\tname: \"GitHub Copilot\",\n\n\tasync login(callbacks) {\n\t\tconst credentials = await loginGitHubCopilot({\n\t\t\tonDeviceCode: (info) => callbacks.notify({ type: \"device_code\", ...info }),\n\t\t\tonPrompt: (prompt) =>\n\t\t\t\tcallbacks.prompt({ type: \"text\", message: prompt.message, placeholder: prompt.placeholder }),\n\t\t\tonProgress: (message) => callbacks.notify({ type: \"progress\", message }),\n\t\t\tsignal: callbacks.signal,\n\t\t});\n\t\treturn { ...credentials, type: \"oauth\" };\n\t},\n\n\tasync refresh(credential) {\n\t\treturn {\n\t\t\t...(await refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential))),\n\t\t\ttype: \"oauth\",\n\t\t};\n\t},\n\n\t/** Per-credential baseUrl from the token's proxy endpoint replaces the old `modifyModels` rewriting. */\n\tasync toAuth(credential) {\n\t\treturn {\n\t\t\tapiKey: credential.access,\n\t\t\tbaseUrl: getGitHubCopilotBaseUrl(credential.access, copilotEnterpriseDomain(credential)),\n\t\t};\n\t},\n};\n\nexport const githubCopilotOAuthProvider: OAuthProviderInterface = {\n\tid: \"github-copilot\",\n\tname: \"GitHub Copilot\",\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginGitHubCopilot({\n\t\t\tonDeviceCode: callbacks.onDeviceCode,\n\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\tonProgress: callbacks.onProgress,\n\t\t\tsignal: callbacks.signal,\n\t\t});\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst creds = credentials as CopilotCredentials;\n\t\treturn refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst creds = credentials as CopilotCredentials;\n\t\tconst domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined;\n\t\tconst baseUrl = getGitHubCopilotBaseUrl(creds.access, domain);\n\t\t// Older stored Pi auth entries do not have account-specific model IDs yet;\n\t\t// keep their existing generated-catalog behavior until the next refresh/login.\n\t\tconst availableModelIds = \"availableModelIds\" in creds ? new Set(creds.availableModelIds) : undefined;\n\n\t\treturn models.flatMap((m) => {\n\t\t\tif (m.provider !== \"github-copilot\") return [m];\n\t\t\tif (availableModelIds && !availableModelIds.has(m.id)) return [];\n\t\t\treturn [{ ...m, baseUrl }];\n\t\t});\n\t},\n};\n"]}
|
|
1
|
+
{"version":3,"file":"github-copilot.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/github-copilot.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,qBAAqB,CAAC;AAItE,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAuB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAsCrH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS5D;AA4BD,wBAAgB,uBAAuB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CASzF;AAgMD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,YAAY,EAAE,MAAM,EACpB,gBAAgB,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AA8CD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAClD,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACvG,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuC5B;AAQD,eAAO,MAAM,kBAAkB,EAAE,SA4BhC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,sBAoCxC,CAAC","sourcesContent":["/**\n * GitHub Copilot OAuth flow\n */\n\nimport type { OAuthAuth, OAuthCredential } from \"../../auth/types.ts\";\nimport { GITHUB_COPILOT_MODELS } from \"../../providers/github-copilot.models.ts\";\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport type { OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\ntype CopilotCredentials = OAuthCredentials & {\n\tenterpriseUrl?: string;\n\tavailableModelIds: string[];\n};\n\nconst decode = (s: string) => atob(s);\nconst CLIENT_ID = decode(\"SXYxLmI1MDdhMDhjODdlY2ZlOTg=\");\n\nconst COPILOT_HEADERS = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n} as const;\nconst COPILOT_API_VERSION = \"2026-06-01\";\n\ntype DeviceCodeResponse = {\n\tdevice_code: string;\n\tuser_code: string;\n\tverification_uri: string;\n\tinterval?: number;\n\texpires_in: number;\n};\n\ntype DeviceTokenSuccessResponse = {\n\taccess_token: string;\n\ttoken_type?: string;\n\tscope?: string;\n};\n\ntype DeviceTokenErrorResponse = {\n\terror: string;\n\terror_description?: string;\n\tinterval?: number;\n};\n\nexport function normalizeDomain(input: string): string | null {\n\tconst trimmed = input.trim();\n\tif (!trimmed) return null;\n\ttry {\n\t\tconst url = trimmed.includes(\"://\") ? new URL(trimmed) : new URL(`https://${trimmed}`);\n\t\treturn url.hostname;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction getUrls(domain: string): {\n\tdeviceCodeUrl: string;\n\taccessTokenUrl: string;\n\tcopilotTokenUrl: string;\n} {\n\treturn {\n\t\tdeviceCodeUrl: `https://${domain}/login/device/code`,\n\t\taccessTokenUrl: `https://${domain}/login/oauth/access_token`,\n\t\tcopilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,\n\t};\n}\n\n/**\n * Parse the proxy-ep from a Copilot token and convert to API base URL.\n * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...\n * Returns API URL like https://api.individual.githubcopilot.com\n */\nfunction getBaseUrlFromToken(token: string): string | null {\n\tconst match = token.match(/proxy-ep=([^;]+)/);\n\tif (!match) return null;\n\tconst proxyHost = match[1];\n\t// Convert proxy.xxx to api.xxx\n\tconst apiHost = proxyHost.replace(/^proxy\\./, \"api.\");\n\treturn `https://${apiHost}`;\n}\n\nexport function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {\n\t// If we have a token, extract the base URL from proxy-ep\n\tif (token) {\n\t\tconst urlFromToken = getBaseUrlFromToken(token);\n\t\tif (urlFromToken) return urlFromToken;\n\t}\n\t// Fallback for enterprise or if token parsing fails\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn \"https://api.individual.githubcopilot.com\";\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction isSelectableCopilotModel(item: Record<string, unknown>): boolean {\n\tconst policy = asRecord(item.policy);\n\tconst capabilities = asRecord(item.capabilities);\n\tconst supports = asRecord(capabilities?.supports);\n\treturn item.model_picker_enabled === true && policy?.state !== \"disabled\" && supports?.tool_calls !== false;\n}\n\nfunction parseAvailableCopilotModelIds(raw: unknown): string[] {\n\tconst data = asRecord(raw)?.data;\n\tif (!Array.isArray(data)) {\n\t\tthrow new Error(\"Invalid Copilot models response\");\n\t}\n\n\tconst ids: string[] = [];\n\tfor (const rawItem of data) {\n\t\tconst item = asRecord(rawItem);\n\t\tconst id = item?.id;\n\t\tif (typeof id === \"string\" && item && isSelectableCopilotModel(item)) {\n\t\t\tids.push(id);\n\t\t}\n\t}\n\treturn ids;\n}\n\nasync function fetchAvailableGitHubCopilotModelIds(copilotToken: string, enterpriseDomain?: string): Promise<string[]> {\n\tconst baseUrl = getGitHubCopilotBaseUrl(copilotToken, enterpriseDomain);\n\tconst raw = await fetchJson(`${baseUrl}/models`, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${copilotToken}`,\n\t\t\t...COPILOT_HEADERS,\n\t\t\t\"X-GitHub-Api-Version\": COPILOT_API_VERSION,\n\t\t},\n\t\tsignal: AbortSignal.timeout(5000),\n\t});\n\treturn parseAvailableCopilotModelIds(raw);\n}\n\nasync function fetchJson(url: string, init: RequestInit): Promise<unknown> {\n\tconst response = await fetch(url, init);\n\tif (!response.ok) {\n\t\tconst text = await response.text();\n\t\tthrow new Error(`${response.status} ${response.statusText}: ${text}`);\n\t}\n\treturn response.json();\n}\n\nasync function startDeviceFlow(domain: string): Promise<DeviceCodeResponse> {\n\tconst urls = getUrls(domain);\n\tconst data = await fetchJson(urls.deviceCodeUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t},\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: \"read:user\",\n\t\t}),\n\t});\n\n\tif (!data || typeof data !== \"object\") {\n\t\tthrow new Error(\"Invalid device code response\");\n\t}\n\n\tconst deviceCode = (data as Record<string, unknown>).device_code;\n\tconst userCode = (data as Record<string, unknown>).user_code;\n\tconst verificationUri = (data as Record<string, unknown>).verification_uri;\n\tconst interval = (data as Record<string, unknown>).interval;\n\tconst expiresIn = (data as Record<string, unknown>).expires_in;\n\n\tif (\n\t\ttypeof deviceCode !== \"string\" ||\n\t\ttypeof userCode !== \"string\" ||\n\t\ttypeof verificationUri !== \"string\" ||\n\t\t(interval !== undefined && typeof interval !== \"number\") ||\n\t\ttypeof expiresIn !== \"number\"\n\t) {\n\t\tthrow new Error(\"Invalid device code response fields\");\n\t}\n\n\t// The verification URI is opened in the user's browser and to prevent `open` from\n\t// opening an executable or similar, we force it to be a URL.\n\tlet parsedUri: URL;\n\ttry {\n\t\tparsedUri = new URL(verificationUri);\n\t} catch {\n\t\tthrow new Error(\"Untrusted verification_uri in device code response\");\n\t}\n\tif (parsedUri.protocol !== \"https:\" && parsedUri.protocol !== \"http:\") {\n\t\tthrow new Error(\"Untrusted verification_uri in device code response\");\n\t}\n\n\treturn {\n\t\tdevice_code: deviceCode,\n\t\tuser_code: userCode,\n\t\tverification_uri: parsedUri.href,\n\t\tinterval,\n\t\texpires_in: expiresIn,\n\t};\n}\n\nasync function pollForGitHubAccessToken(\n\tdomain: string,\n\tdevice: DeviceCodeResponse,\n\tsignal?: AbortSignal,\n): Promise<string> {\n\tconst urls = getUrls(domain);\n\treturn pollOAuthDeviceCodeFlow<string>({\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in,\n\t\twaitBeforeFirstPoll: true,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst raw = await fetchJson(urls.accessTokenUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: device.device_code,\n\t\t\t\t\tgrant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tif (raw && typeof raw === \"object\" && typeof (raw as DeviceTokenSuccessResponse).access_token === \"string\") {\n\t\t\t\treturn { status: \"complete\", value: (raw as DeviceTokenSuccessResponse).access_token };\n\t\t\t}\n\n\t\t\tif (raw && typeof raw === \"object\" && typeof (raw as DeviceTokenErrorResponse).error === \"string\") {\n\t\t\t\tconst { error, error_description: description, interval } = raw as DeviceTokenErrorResponse;\n\t\t\t\tif (error === \"authorization_pending\") {\n\t\t\t\t\treturn { status: \"pending\" };\n\t\t\t\t}\n\n\t\t\t\tif (error === \"slow_down\") {\n\t\t\t\t\treturn { status: \"slow_down\", intervalSeconds: typeof interval === \"number\" ? interval : undefined };\n\t\t\t\t}\n\n\t\t\t\tconst descriptionSuffix = description ? `: ${description}` : \"\";\n\t\t\t\treturn { status: \"failed\", message: `Device flow failed: ${error}${descriptionSuffix}` };\n\t\t\t}\n\n\t\t\treturn { status: \"failed\", message: \"Invalid device token response\" };\n\t\t},\n\t});\n}\n\nasync function refreshGitHubCopilotAccessToken(\n\trefreshToken: string,\n\tenterpriseDomain?: string,\n): Promise<OAuthCredentials> {\n\tconst domain = enterpriseDomain || \"github.com\";\n\tconst urls = getUrls(domain);\n\n\tconst raw = await fetchJson(urls.copilotTokenUrl, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${refreshToken}`,\n\t\t\t...COPILOT_HEADERS,\n\t\t},\n\t});\n\n\tif (!raw || typeof raw !== \"object\") {\n\t\tthrow new Error(\"Invalid Copilot token response\");\n\t}\n\n\tconst token = (raw as Record<string, unknown>).token;\n\tconst expiresAt = (raw as Record<string, unknown>).expires_at;\n\n\tif (typeof token !== \"string\" || typeof expiresAt !== \"number\") {\n\t\tthrow new Error(\"Invalid Copilot token response fields\");\n\t}\n\n\treturn {\n\t\trefresh: refreshToken,\n\t\taccess: token,\n\t\texpires: expiresAt * 1000 - 5 * 60 * 1000,\n\t\tenterpriseUrl: enterpriseDomain,\n\t};\n}\n\n/**\n * Refresh GitHub Copilot token\n */\nexport async function refreshGitHubCopilotToken(\n\trefreshToken: string,\n\tenterpriseDomain?: string,\n): Promise<OAuthCredentials> {\n\tconst credentials = await refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain);\n\treturn {\n\t\t...credentials,\n\t\tavailableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain),\n\t};\n}\n\n/**\n * Enable a model for the user's GitHub Copilot account.\n * This is required for some models (like Claude, Grok) before they can be used.\n */\nasync function enableGitHubCopilotModel(token: string, modelId: string, enterpriseDomain?: string): Promise<boolean> {\n\tconst baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);\n\tconst url = `${baseUrl}/models/${modelId}/policy`;\n\n\ttry {\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${token}`,\n\t\t\t\t...COPILOT_HEADERS,\n\t\t\t\t\"openai-intent\": \"chat-policy\",\n\t\t\t\t\"x-interaction-type\": \"chat-policy\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({ state: \"enabled\" }),\n\t\t});\n\t\treturn response.ok;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Enable all known GitHub Copilot models that may require policy acceptance.\n * Called after successful login to ensure all models are available.\n */\nasync function enableAllGitHubCopilotModels(\n\ttoken: string,\n\tenterpriseDomain?: string,\n\tonProgress?: (model: string, success: boolean) => void,\n): Promise<void> {\n\tconst models = Object.values(GITHUB_COPILOT_MODELS);\n\tawait Promise.all(\n\t\tmodels.map(async (model) => {\n\t\t\tconst success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);\n\t\t\tonProgress?.(model.id, success);\n\t\t}),\n\t);\n}\n\n/**\n * Login with GitHub Copilot OAuth (device code flow)\n *\n * @param options.onDeviceCode - Callback with URL and user code\n * @param options.onPrompt - Callback to prompt user for input\n * @param options.onProgress - Optional progress callback\n * @param options.signal - Optional AbortSignal for cancellation\n */\nexport async function loginGitHubCopilot(options: {\n\tonDeviceCode: (info: OAuthDeviceCodeInfo) => void;\n\tonPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;\n\tonProgress?: (message: string) => void;\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst input = await options.onPrompt({\n\t\tmessage: \"GitHub Enterprise URL/domain (blank for github.com)\",\n\t\tplaceholder: \"company.ghe.com\",\n\t\tallowEmpty: true,\n\t});\n\n\tif (options.signal?.aborted) {\n\t\tthrow new Error(\"Login cancelled\");\n\t}\n\n\tconst trimmed = input.trim();\n\tconst enterpriseDomain = normalizeDomain(input);\n\tif (trimmed && !enterpriseDomain) {\n\t\tthrow new Error(\"Invalid GitHub Enterprise URL/domain\");\n\t}\n\tconst domain = enterpriseDomain || \"github.com\";\n\n\tconst device = await startDeviceFlow(domain);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code,\n\t\tverificationUri: device.verification_uri,\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in,\n\t});\n\n\tconst githubAccessToken = await pollForGitHubAccessToken(domain, device, options.signal);\n\tconst credentials = await refreshGitHubCopilotAccessToken(githubAccessToken, enterpriseDomain ?? undefined);\n\n\t// Enable all models after successful login\n\toptions.onProgress?.(\"Enabling models...\");\n\tawait enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);\n\n\t// Fetch availability after policy enable so newly enabled models are included,\n\t// while unavailable models are still filtered out.\n\treturn {\n\t\t...credentials,\n\t\tavailableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain ?? undefined),\n\t};\n}\n\nfunction copilotEnterpriseDomain(credential: OAuthCredential): string | undefined {\n\tconst enterpriseUrl = credential.enterpriseUrl;\n\tif (typeof enterpriseUrl !== \"string\" || !enterpriseUrl) return undefined;\n\treturn normalizeDomain(enterpriseUrl) ?? undefined;\n}\n\nexport const githubCopilotOAuth: OAuthAuth = {\n\tname: \"GitHub Copilot\",\n\n\tasync login(callbacks) {\n\t\tconst credentials = await loginGitHubCopilot({\n\t\t\tonDeviceCode: (info) => callbacks.notify({ type: \"device_code\", ...info }),\n\t\t\tonPrompt: (prompt) =>\n\t\t\t\tcallbacks.prompt({ type: \"text\", message: prompt.message, placeholder: prompt.placeholder }),\n\t\t\tonProgress: (message) => callbacks.notify({ type: \"progress\", message }),\n\t\t\tsignal: callbacks.signal,\n\t\t});\n\t\treturn { ...credentials, type: \"oauth\" };\n\t},\n\n\tasync refresh(credential) {\n\t\treturn {\n\t\t\t...(await refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential))),\n\t\t\ttype: \"oauth\",\n\t\t};\n\t},\n\n\t/** Per-credential baseUrl from the token's proxy endpoint replaces the old `modifyModels` rewriting. */\n\tasync toAuth(credential) {\n\t\treturn {\n\t\t\tapiKey: credential.access,\n\t\t\tbaseUrl: getGitHubCopilotBaseUrl(credential.access, copilotEnterpriseDomain(credential)),\n\t\t};\n\t},\n};\n\nexport const githubCopilotOAuthProvider: OAuthProviderInterface = {\n\tid: \"github-copilot\",\n\tname: \"GitHub Copilot\",\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginGitHubCopilot({\n\t\t\tonDeviceCode: callbacks.onDeviceCode,\n\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\tonProgress: callbacks.onProgress,\n\t\t\tsignal: callbacks.signal,\n\t\t});\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst creds = credentials as CopilotCredentials;\n\t\treturn refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst creds = credentials as CopilotCredentials;\n\t\tconst domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined;\n\t\tconst baseUrl = getGitHubCopilotBaseUrl(creds.access, domain);\n\t\t// Older stored Pi auth entries do not have account-specific model IDs yet;\n\t\t// keep their existing generated-catalog behavior until the next refresh/login.\n\t\tconst availableModelIds = \"availableModelIds\" in creds ? new Set(creds.availableModelIds) : undefined;\n\n\t\treturn models.flatMap((m) => {\n\t\t\tif (m.provider !== \"github-copilot\") return [m];\n\t\t\tif (availableModelIds && !availableModelIds.has(m.id)) return [];\n\t\t\treturn [{ ...m, baseUrl }];\n\t\t});\n\t},\n};\n"]}
|
|
@@ -156,6 +156,7 @@ async function pollForGitHubAccessToken(domain, device, signal) {
|
|
|
156
156
|
return pollOAuthDeviceCodeFlow({
|
|
157
157
|
intervalSeconds: device.interval,
|
|
158
158
|
expiresInSeconds: device.expires_in,
|
|
159
|
+
waitBeforeFirstPoll: true,
|
|
159
160
|
signal,
|
|
160
161
|
poll: async () => {
|
|
161
162
|
const raw = await fetchJson(urls.accessTokenUrl, {
|
|
@@ -175,12 +176,12 @@ async function pollForGitHubAccessToken(domain, device, signal) {
|
|
|
175
176
|
return { status: "complete", value: raw.access_token };
|
|
176
177
|
}
|
|
177
178
|
if (raw && typeof raw === "object" && typeof raw.error === "string") {
|
|
178
|
-
const { error, error_description: description } = raw;
|
|
179
|
+
const { error, error_description: description, interval } = raw;
|
|
179
180
|
if (error === "authorization_pending") {
|
|
180
181
|
return { status: "pending" };
|
|
181
182
|
}
|
|
182
183
|
if (error === "slow_down") {
|
|
183
|
-
return { status: "slow_down" };
|
|
184
|
+
return { status: "slow_down", intervalSeconds: typeof interval === "number" ? interval : undefined };
|
|
184
185
|
}
|
|
185
186
|
const descriptionSuffix = description ? `: ${description}` : "";
|
|
186
187
|
return { status: "failed", message: `Device flow failed: ${error}${descriptionSuffix}` };
|