@earendil-works/pi-ai 1.5.0 → 1.5.1
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 +23 -4
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +224 -17
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/azure-openai-responses.js +1 -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 +4 -2
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/bedrock-converse-stream.lazy.d.ts +7 -0
- package/dist/api/bedrock-converse-stream.lazy.d.ts.map +1 -1
- package/dist/api/bedrock-converse-stream.lazy.js +18 -5
- package/dist/api/bedrock-converse-stream.lazy.js.map +1 -1
- package/dist/api/constrained-sampling.d.ts +0 -2
- package/dist/api/constrained-sampling.d.ts.map +1 -1
- package/dist/api/constrained-sampling.js +0 -13
- package/dist/api/constrained-sampling.js.map +1 -1
- package/dist/api/lazy.d.ts +1 -1
- package/dist/api/lazy.d.ts.map +1 -1
- package/dist/api/lazy.js +9 -8
- package/dist/api/lazy.js.map +1 -1
- package/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/dist/api/openai-codex-responses.js +7 -3
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts +2 -1
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +13 -5
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses.d.ts.map +1 -1
- package/dist/api/openai-responses.js +4 -2
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/api/simple-options.d.ts +1 -0
- package/dist/api/simple-options.d.ts.map +1 -1
- package/dist/api/simple-options.js +16 -2
- package/dist/api/simple-options.js.map +1 -1
- package/dist/api/strict-tool-limits.d.ts +13 -0
- package/dist/api/strict-tool-limits.d.ts.map +1 -0
- package/dist/api/strict-tool-limits.js +50 -0
- package/dist/api/strict-tool-limits.js.map +1 -0
- package/dist/auth/oauth/load.d.ts.map +1 -1
- package/dist/auth/oauth/load.js +5 -2
- package/dist/auth/oauth/load.js.map +1 -1
- package/dist/cache-warming.d.ts +53 -0
- package/dist/cache-warming.d.ts.map +1 -0
- package/dist/cache-warming.js +82 -0
- package/dist/cache-warming.js.map +1 -0
- package/dist/env-api-keys.d.ts.map +1 -1
- package/dist/env-api-keys.js +1 -0
- package/dist/env-api-keys.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lazy-bedrock-provider.d.ts +10 -0
- package/dist/lazy-bedrock-provider.d.ts.map +1 -0
- package/dist/lazy-bedrock-provider.js +13 -0
- package/dist/lazy-bedrock-provider.js.map +1 -0
- package/dist/lazy-oauth.d.ts +10 -0
- package/dist/lazy-oauth.d.ts.map +1 -0
- package/dist/lazy-oauth.js +23 -0
- package/dist/lazy-oauth.js.map +1 -0
- package/dist/models.d.ts +7 -0
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +4 -0
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js +4 -0
- package/dist/models.generated.js.map +1 -1
- package/dist/models.js +19 -11
- package/dist/models.js.map +1 -1
- package/dist/providers/all.d.ts.map +1 -1
- package/dist/providers/all.js +2 -0
- package/dist/providers/all.js.map +1 -1
- package/dist/providers/compat-schema.d.ts +16 -0
- package/dist/providers/compat-schema.d.ts.map +1 -1
- package/dist/providers/compat-schema.js +22 -1
- package/dist/providers/compat-schema.js.map +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/amazon-bedrock.json +1 -1
- package/dist/providers/data/anthropic.json +1 -1
- package/dist/providers/data/azure.json +1 -1
- package/dist/providers/data/baseten.json +1 -1
- package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/dist/providers/data/fireworks.json +1 -1
- package/dist/providers/data/github-copilot.json +1 -1
- package/dist/providers/data/google.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/opencode-go.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/qwen-token-plan-individual-cn.json +1 -0
- package/dist/providers/data/qwen-token-plan-individual.json +1 -1
- package/dist/providers/data/radius.json +1 -1
- package/dist/providers/data/together.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/providers/github-copilot.d.ts +3 -1
- package/dist/providers/github-copilot.d.ts.map +1 -1
- package/dist/providers/github-copilot.js +13 -2
- package/dist/providers/github-copilot.js.map +1 -1
- package/dist/providers/openai-codex.js +1 -1
- package/dist/providers/openai-codex.js.map +1 -1
- package/dist/providers/qwen-token-plan-individual-cn.d.ts +3 -0
- package/dist/providers/qwen-token-plan-individual-cn.d.ts.map +1 -0
- package/dist/providers/qwen-token-plan-individual-cn.js +15 -0
- package/dist/providers/qwen-token-plan-individual-cn.js.map +1 -0
- package/dist/providers/qwen-token-plan-individual-cn.models.d.ts +6 -0
- package/dist/providers/qwen-token-plan-individual-cn.models.d.ts.map +1 -0
- package/dist/providers/qwen-token-plan-individual-cn.models.js +8 -0
- package/dist/providers/qwen-token-plan-individual-cn.models.js.map +1 -0
- package/dist/types.d.ts +8 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/error-body.d.ts +8 -0
- package/dist/utils/error-body.d.ts.map +1 -1
- package/dist/utils/error-body.js +28 -1
- package/dist/utils/error-body.js.map +1 -1
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +3 -0
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/version.d.ts +1 -1
- package/dist/utils/version.js +1 -1
- package/dist/utils/version.js.map +1 -1
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and
|
|
|
64
64
|
- **OpenAI** (including the Decisions classifier API)
|
|
65
65
|
- **Ant Ling**
|
|
66
66
|
- **Azure OpenAI (Responses)**
|
|
67
|
-
- **OpenAI Codex
|
|
67
|
+
- **OpenAI Codex** (ChatGPT Plus/Pro subscription, requires OAuth, see below)
|
|
68
68
|
- **Radius** (API key or OAuth, with a dynamically refreshed gateway catalog)
|
|
69
69
|
- **TypeSafe** (System One classifier API)
|
|
70
70
|
- **DeepSeek**
|
|
@@ -524,6 +524,7 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
|
|
|
524
524
|
| Qwen Token Plan (existing catalog) | `QWEN_TOKEN_PLAN_API_KEY` |
|
|
525
525
|
| Qwen Token Plan (Individual) | `QWEN_TOKEN_PLAN_API_KEY` |
|
|
526
526
|
| Qwen Token Plan (China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |
|
|
527
|
+
| Qwen Token Plan (Individual, China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |
|
|
527
528
|
| Xiaomi MiMo (API billing) | `XIAOMI_API_KEY` |
|
|
528
529
|
| Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` |
|
|
529
530
|
| Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` |
|
|
@@ -533,6 +534,8 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
|
|
|
533
534
|
`qwen-token-plan-individual` and `qwen-token-plan` share the international endpoint and
|
|
534
535
|
`QWEN_TOKEN_PLAN_API_KEY`. The Individual provider exposes only the models documented for Individual
|
|
535
536
|
subscriptions, while the existing provider retains its broader catalog for backward compatibility.
|
|
537
|
+
`qwen-token-plan-individual-cn` applies the same Individual catalog to the China endpoint and
|
|
538
|
+
`QWEN_TOKEN_PLAN_CN_API_KEY`.
|
|
536
539
|
Stored credentials remain provider-scoped, so save the key under the provider ID you register.
|
|
537
540
|
|
|
538
541
|
Amazon Bedrock resolves ambient AWS credentials (`AWS_PROFILE`, access key pairs, `AWS_BEARER_TOKEN_BEDROCK`, ECS task roles, web identity tokens); its provider-owned login flow supports bearer tokens, AWS profiles, and the existing credential chain. Vertex AI resolves either an explicit key or gcloud Application Default Credentials plus project/location, with a provider-owned login flow for API keys, ADC, and service-account files.
|
|
@@ -891,7 +894,7 @@ console.log(model.output); // ['image'] or ['image', 'text']
|
|
|
891
894
|
|
|
892
895
|
## Classification
|
|
893
896
|
|
|
894
|
-
Classifier models consume structured JSON state, and optionally images, and answer one or more typed questions. They do not use chat or image-generation APIs. TypeSafe's Jev model, Cloudflare's Clef models,
|
|
897
|
+
Classifier models consume structured JSON state, and optionally images, and answer one or more typed questions. They do not use chat or image-generation APIs. TypeSafe's Jev model, Cloudflare's Clef models, OpenAI's GPT-6 Luna, and Microsoft-Decision-1 are available from these built-in providers:
|
|
895
898
|
|
|
896
899
|
| Provider | Model IDs | Auth |
|
|
897
900
|
| --- | --- | --- |
|
|
@@ -901,6 +904,7 @@ Classifier models consume structured JSON state, and optionally images, and answ
|
|
|
901
904
|
| `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
|
|
902
905
|
| `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
|
|
903
906
|
| `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |
|
|
907
|
+
| `github-copilot` | `microsoft-decision-1` | `COPILOT_GITHUB_TOKEN` or GitHub Copilot OAuth |
|
|
904
908
|
|
|
905
909
|
```typescript
|
|
906
910
|
import { builtinModels } from '@earendil-works/pi-ai/providers/all';
|
|
@@ -1437,6 +1441,8 @@ Built-in API implementations live under `./api/<api-id>`:
|
|
|
1437
1441
|
|
|
1438
1442
|
Importing an implementation module loads its SDK. The `./api/<id>.lazy` wrappers (used by the provider factories) defer that load to the first request when the runtime or bundler supports dynamic import chunking. Legacy raw API subpaths from older releases (`./anthropic`, `./google`, `./mistral`, `./openai-completions`, ...) were removed; use `@earendil-works/pi-ai/api/<api-id>`.
|
|
1439
1443
|
|
|
1444
|
+
**Anthropic message threads:** with `sessionId` set and `cacheRetention` not `"none"`, Anthropic requests continue from the previous reply on the server (`message-threads-2026-08-12` beta) and send only the new messages. When anything earlier in the request changed, or the server can't continue, the full request is sent. Token counts and prompt caching are unchanged. On by default only for the `anthropic` provider on `api.anthropic.com`; set `compat.supportsMessageThreads` to change that, for example for gateways that forward the `thread` field. Set `disableContinuation: true` on requests that repeat an earlier one, such as cache warming. Call `cleanupSessionResources(sessionId)` when a session ends.
|
|
1445
|
+
|
|
1440
1446
|
### OpenAI Compatibility Settings
|
|
1441
1447
|
|
|
1442
1448
|
The `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for a small set of known OpenAI-compatible providers (Cerebras, xAI, Chutes, DeepSeek, NVIDIA NIM, Together AI, zAi, OpenCode, Cloudflare Workers AI, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field. For `openai-responses` models, the compat field supports Responses-specific flags.
|
|
@@ -1456,6 +1462,7 @@ interface OpenAICompletionsCompat {
|
|
|
1456
1462
|
maxTokensField?: 'max_completion_tokens' | 'max_tokens'; // Which field name to use (default: max_completion_tokens)
|
|
1457
1463
|
requiresToolResultName?: boolean; // Whether tool results require the `name` field (default: false)
|
|
1458
1464
|
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
|
|
1465
|
+
requiresNonNullAssistantContent?: boolean; // Whether replayed assistant messages with no text must send "" instead of null content (default: false)
|
|
1459
1466
|
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
|
|
1460
1467
|
requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
|
|
1461
1468
|
thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'baseten' | 'zai' | 'qwen' | 'chat-template' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: '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, 'baseten' uses configurable chat_template_args plus reasoning_effort when supported, 'zai' uses thinking: { type }, 'qwen' uses enable_thinking, 'chat-template' uses configurable chat_template_kwargs, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking and preserve_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai)
|
|
@@ -1764,6 +1771,18 @@ setBedrockProviderModule(bedrockProviderModule);
|
|
|
1764
1771
|
|
|
1765
1772
|
That explicit override bundles the AWS SDK. Without it, Bedrock's opaque runtime import expects the package's Bedrock implementation file to be available at runtime.
|
|
1766
1773
|
|
|
1774
|
+
Node bundles that split at dynamic imports (esbuild `splitting: true`) can keep Bedrock and the OAuth login flows inside the bundle but lazy: register loaders with literal dynamic imports, which the bundler turns into chunks that load on first use:
|
|
1775
|
+
|
|
1776
|
+
```typescript
|
|
1777
|
+
import { registerLazyBedrockProvider } from '@earendil-works/pi-ai/lazy-bedrock-provider';
|
|
1778
|
+
import { registerLazyOAuthFlows } from '@earendil-works/pi-ai/lazy-oauth';
|
|
1779
|
+
|
|
1780
|
+
registerLazyBedrockProvider();
|
|
1781
|
+
registerLazyOAuthFlows();
|
|
1782
|
+
```
|
|
1783
|
+
|
|
1784
|
+
Both modules are Node-only; never import them into a browser bundle.
|
|
1785
|
+
|
|
1767
1786
|
### Provider-Scoped Environment Overrides
|
|
1768
1787
|
|
|
1769
1788
|
Pass `env` in stream options to scope provider configuration to a request. Values in `env` are used before process environment variables for provider auth and configuration such as Cloudflare account IDs, Azure OpenAI settings, Vertex project/location, Bedrock settings, `PI_CACHE_RETENTION`, and `HTTP_PROXY`/`HTTPS_PROXY`.
|
|
@@ -1789,7 +1808,7 @@ Several providers support OAuth authentication instead of static API keys:
|
|
|
1789
1808
|
|
|
1790
1809
|
- **Anthropic** (Claude Pro/Max subscription)
|
|
1791
1810
|
- **OpenAI** (Sign in with ChatGPT: uses the ChatGPT subscription with the OpenAI API)
|
|
1792
|
-
- **OpenAI Codex
|
|
1811
|
+
- **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)
|
|
1793
1812
|
- **GitHub Copilot** (Copilot subscription)
|
|
1794
1813
|
- **OpenRouter** (OAuth PKCE that mints a user-controlled API key)
|
|
1795
1814
|
|
|
@@ -1869,7 +1888,7 @@ Built-in login and refresh flows are private provider implementations. Use provi
|
|
|
1869
1888
|
|
|
1870
1889
|
Provider notes:
|
|
1871
1890
|
|
|
1872
|
-
**OpenAI Codex
|
|
1891
|
+
**OpenAI Codex**: Superseded by Sign in with ChatGPT on the OpenAI provider. Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options unless `cacheRetention` is `"none"`. You can set `transport` in stream options to `"sse"`, `"websocket"`, or `"auto"` for Codex Responses transport selection. When using WebSocket with a `sessionId` and cache retention enabled, connections are reused per session and expire after 5 minutes of inactivity. Call `cleanupSessionResources(sessionId)` when finished so the pooled connection does not keep the process alive.
|
|
1873
1892
|
|
|
1874
1893
|
**Azure OpenAI (Responses)**: Uses the Responses API only. Set `AZURE_OPENAI_API_KEY` and either `AZURE_OPENAI_BASE_URL` or `AZURE_OPENAI_RESOURCE_NAME`. `AZURE_OPENAI_BASE_URL` supports both `https://<resource>.openai.azure.com` and `https://<resource>.cognitiveservices.azure.com`; root endpoints are normalized to `.../openai/v1` automatically. Use `AZURE_OPENAI_API_VERSION` (defaults to `v1`) to override the API version if needed. Deployment names are treated as model IDs by default, override with `azureDeploymentName` or `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` using comma-separated `model-id=deployment` pairs (for example `gpt-4o-mini=my-deployment,gpt-4o=prod`). Legacy deployment-based URLs are intentionally unsupported.
|
|
1875
1894
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAiC,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAiC,MAAM,mBAAmB,CAAC;AAqBlE,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAIb,MAAM,aAAa,CAAC;AAqKrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA6QhE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAoRD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CAiXzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAgDlF,CAAC"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import Anthropic, {} from "@anthropic-ai/sdk";
|
|
2
2
|
import { ANTHROPIC_FEDERATION_RULE_ID_ENV, ANTHROPIC_IDENTITY_TOKEN_FILE_ENV, ANTHROPIC_ORGANIZATION_ID_ENV, ANTHROPIC_SERVICE_ACCOUNT_ID_ENV, ANTHROPIC_WORKSPACE_ID_ENV, } from "../env-api-keys.js";
|
|
3
3
|
import { calculateCost } from "../models.js";
|
|
4
|
+
import { registerSessionResourceCleanup } from "../session-resources.js";
|
|
4
5
|
import { appendAssistantMessageDiagnostic } from "../utils/diagnostics.js";
|
|
6
|
+
import { withCauseDetail } from "../utils/error-body.js";
|
|
5
7
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
8
|
+
import { shortHash } from "../utils/hash.js";
|
|
6
9
|
import { headersToRecord } from "../utils/headers.js";
|
|
7
10
|
import { getIdentityHeaders } from "../utils/identity.js";
|
|
8
11
|
import { parseJsonWithRepair } from "../utils/json-parse.js";
|
|
@@ -13,9 +16,10 @@ import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
|
|
13
16
|
import { ToolCallArgumentsStream } from "../utils/streaming-json.js";
|
|
14
17
|
import { getSystemMessageText, renderSystemMessageUpdate } from "../utils/text.js";
|
|
15
18
|
import { getCurrentTools, getInitialSystemMessage, resolveTranscript, } from "../utils/transcript.js";
|
|
16
|
-
import { getJsonSchemaToolParameters,
|
|
19
|
+
import { getJsonSchemaToolParameters, prefersJsonSchemaStrictSampling, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
|
|
17
20
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
|
18
21
|
import { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from "./simple-options.js";
|
|
22
|
+
import { hasExceededStrictToolLimits, isStrictToolLimitErrorMessage, markStrictToolLimitsExceeded, } from "./strict-tool-limits.js";
|
|
19
23
|
import { splitErrorToolResultImages, transformMessages } from "./transform-messages.js";
|
|
20
24
|
/**
|
|
21
25
|
* Resolve cache retention preference.
|
|
@@ -122,6 +126,7 @@ const SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-07-01";
|
|
|
122
126
|
const MID_CONVERSATION_OUTPUT_CONFIG_BETA = "mid-conversation-output-config-2026-07-01";
|
|
123
127
|
const THINKING_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
|
|
124
128
|
const INLINE_TOOLS_BETA = "inline-tools-2026-09-15";
|
|
129
|
+
const MESSAGE_THREADS_BETA = "message-threads-2026-08-12";
|
|
125
130
|
/**
|
|
126
131
|
* Stable deferred tool declared whenever native tool changes are in use. Anthropic adds
|
|
127
132
|
* hidden prompt scaffolding for mid-conversation tool changes; declaring this placeholder
|
|
@@ -151,8 +156,178 @@ function getAnthropicCompat(model) {
|
|
|
151
156
|
supportsStrictTools: model.compat?.supportsStrictTools ?? false,
|
|
152
157
|
supportsMidConvoSystemMessages: model.compat?.supportsMidConvoSystemMessages ?? false,
|
|
153
158
|
supportsMidConvoToolChanges: model.compat?.supportsMidConvoToolChanges ?? false,
|
|
159
|
+
// Gateways that drop unknown request fields would silently turn a `continue` into a request
|
|
160
|
+
// without the earlier conversation, so the default is limited to Anthropic's own endpoint.
|
|
161
|
+
// Widen or remove this restriction once proxies generally forward the `thread` field.
|
|
162
|
+
supportsMessageThreads: model.compat?.supportsMessageThreads ??
|
|
163
|
+
(model.provider === "anthropic" && model.baseUrl.includes("api.anthropic.com")),
|
|
154
164
|
};
|
|
155
165
|
}
|
|
166
|
+
/** Per session: where the next request can continue, or "off" after the endpoint rejected threads. */
|
|
167
|
+
const threadState = new Map();
|
|
168
|
+
const MAX_THREAD_STATE_ENTRIES = 10_000;
|
|
169
|
+
/** Rejections of a `continue` that a new thread fixes: expired, raced, or a fixed setting changed. */
|
|
170
|
+
const NEW_THREAD_ERRORS = new Set([
|
|
171
|
+
"thread_not_found",
|
|
172
|
+
"thread_already_continued",
|
|
173
|
+
"thread_continue_in_flight",
|
|
174
|
+
"thread_fingerprint_mismatch",
|
|
175
|
+
]);
|
|
176
|
+
/**
|
|
177
|
+
* Sets a session's thread state, keeping at most `MAX_THREAD_STATE_ENTRIES` sessions.
|
|
178
|
+
*
|
|
179
|
+
* State is normally freed by `cleanupSessionResources(sessionId)` when a session ends, as pi's
|
|
180
|
+
* coding agent does. The cap bounds memory for library users that never call it, for example a
|
|
181
|
+
* server passing a new `sessionId` per conversation. Dropping the least recently updated session
|
|
182
|
+
* is cheap: its thread has most likely expired on the server already (after a few minutes idle),
|
|
183
|
+
* and otherwise its next request just starts a new thread.
|
|
184
|
+
*/
|
|
185
|
+
function setThreadState(sessionId, value) {
|
|
186
|
+
// Re-insert so Map order stays oldest-first for the cap.
|
|
187
|
+
threadState.delete(sessionId);
|
|
188
|
+
threadState.set(sessionId, value);
|
|
189
|
+
if (threadState.size > MAX_THREAD_STATE_ENTRIES) {
|
|
190
|
+
const oldest = threadState.keys().next().value;
|
|
191
|
+
if (oldest !== undefined)
|
|
192
|
+
threadState.delete(oldest);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
registerSessionResourceCleanup((sessionId) => {
|
|
196
|
+
if (sessionId)
|
|
197
|
+
threadState.delete(sessionId);
|
|
198
|
+
else
|
|
199
|
+
threadState.clear();
|
|
200
|
+
});
|
|
201
|
+
/**
|
|
202
|
+
* Hash of everything a `continue` builds on: the settings that must stay the same within a thread
|
|
203
|
+
* and the messages the server already holds.
|
|
204
|
+
*
|
|
205
|
+
* - Settings, as sent: the endpoint (a thread exists on one endpoint only), and the fixed request
|
|
206
|
+
* fields. A `continue` omits `system` and `tools` and the server keeps the thread's own, so only
|
|
207
|
+
* this check can notice a change to them, including their cache markers. The server rejects
|
|
208
|
+
* changes to the other fields with `thread_fingerprint_mismatch`; checking them here saves that
|
|
209
|
+
* rejected request. A fixed field missing here costs only that rejection, which also starts a
|
|
210
|
+
* new thread.
|
|
211
|
+
* - Messages, normalized so messages that mean the same to the API hash the same.
|
|
212
|
+
*/
|
|
213
|
+
function hashThreadPrefix(model, params, messages) {
|
|
214
|
+
const { system, tools, thinking, output_config, tool_choice, betas, fallbacks } = params;
|
|
215
|
+
const settings = JSON.stringify([
|
|
216
|
+
model.provider,
|
|
217
|
+
model.baseUrl,
|
|
218
|
+
params.model,
|
|
219
|
+
system,
|
|
220
|
+
tools,
|
|
221
|
+
thinking,
|
|
222
|
+
output_config,
|
|
223
|
+
tool_choice,
|
|
224
|
+
betas,
|
|
225
|
+
fallbacks,
|
|
226
|
+
]);
|
|
227
|
+
const conversation = JSON.stringify(messages, (key, value) => {
|
|
228
|
+
// Cache markers don't change what a message means, but they move: pi puts one on the newest
|
|
229
|
+
// message, so the message that had it in the last request has none in this one. Extensions
|
|
230
|
+
// may add markers anywhere too.
|
|
231
|
+
if (key === "cache_control")
|
|
232
|
+
return undefined;
|
|
233
|
+
// To attach that marker, the newest user message's string content is sent as one text block;
|
|
234
|
+
// the next request sends the same message as a string: "hi" vs [{ type: "text", text: "hi" }].
|
|
235
|
+
if (key === "content" && typeof value === "string")
|
|
236
|
+
return [{ type: "text", text: value }];
|
|
237
|
+
return value;
|
|
238
|
+
});
|
|
239
|
+
return shortHash(settings + conversation);
|
|
240
|
+
}
|
|
241
|
+
function createThreadRequest(sessionId, params) {
|
|
242
|
+
return { sessionId, body: { ...params, thread: { type: "create" } } };
|
|
243
|
+
}
|
|
244
|
+
function prepareThreadRequest(model, sessionId, params) {
|
|
245
|
+
const state = threadState.get(sessionId);
|
|
246
|
+
if (state === "off")
|
|
247
|
+
return undefined;
|
|
248
|
+
if (state !== undefined &&
|
|
249
|
+
params.messages.length > state.prefixCount &&
|
|
250
|
+
hashThreadPrefix(model, params, params.messages.slice(0, state.prefixCount)) === state.prefixHash) {
|
|
251
|
+
// Each reply can be continued once; deleting also keeps a concurrent request from trying.
|
|
252
|
+
threadState.delete(sessionId);
|
|
253
|
+
const { system: _system, tools: _tools, ...rest } = params;
|
|
254
|
+
return {
|
|
255
|
+
body: {
|
|
256
|
+
...rest,
|
|
257
|
+
messages: params.messages.slice(state.prefixCount),
|
|
258
|
+
thread: { type: "continue", previous_message_id: state.replyId },
|
|
259
|
+
},
|
|
260
|
+
sessionId,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return createThreadRequest(sessionId, params);
|
|
264
|
+
}
|
|
265
|
+
/** The `error_code` Anthropic puts in `error.details` of thread rejections. */
|
|
266
|
+
function getThreadErrorCode(error) {
|
|
267
|
+
const body = error.error;
|
|
268
|
+
const code = body?.error?.details?.error_code;
|
|
269
|
+
return typeof code === "string" ? code : undefined;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Sends a thread request. A rejection about the thread (never a started stream) is resent once in
|
|
273
|
+
* full: as a new thread when the old one can't be continued, or without `thread` when the endpoint
|
|
274
|
+
* can't use threads, which also turns threads off for the session. Other errors are passed on.
|
|
275
|
+
* Returns the response and the thread request that produced it, if any.
|
|
276
|
+
*/
|
|
277
|
+
async function sendThreadRequest(thread, params, send, output, signal) {
|
|
278
|
+
try {
|
|
279
|
+
return { response: await send(thread.body), thread };
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
if (signal?.aborted || !(error instanceof Anthropic.APIError))
|
|
283
|
+
throw error;
|
|
284
|
+
const errorCode = getThreadErrorCode(error);
|
|
285
|
+
const startNewThread = errorCode !== undefined && NEW_THREAD_ERRORS.has(errorCode);
|
|
286
|
+
// The endpoint can't use threads: it can't thread this request, or it doesn't know the
|
|
287
|
+
// `thread` field and rejects it without a code ("thread: Extra inputs are not permitted"),
|
|
288
|
+
// e.g. with an `anthropic-beta` header that lacks the threads beta.
|
|
289
|
+
const threadsUnsupported = errorCode === "thread_unsupported_request" ||
|
|
290
|
+
(errorCode === undefined && error.status === 400 && error.message.includes("thread"));
|
|
291
|
+
// Any other error (prompt too long, unknown model, strict tool limits, ...) is not about the
|
|
292
|
+
// thread, and resending would only fail again.
|
|
293
|
+
if (!startNewThread && !threadsUnsupported)
|
|
294
|
+
throw error;
|
|
295
|
+
// Recorded before the resend, so a failed resend keeps it.
|
|
296
|
+
appendAssistantMessageDiagnostic(output, {
|
|
297
|
+
type: "anthropic_thread_fallback",
|
|
298
|
+
timestamp: Date.now(),
|
|
299
|
+
error: { message: error.message },
|
|
300
|
+
details: { reason: errorCode ?? String(error.status) },
|
|
301
|
+
});
|
|
302
|
+
if (startNewThread) {
|
|
303
|
+
const created = createThreadRequest(thread.sessionId, params);
|
|
304
|
+
return { response: await send(created.body), thread: created };
|
|
305
|
+
}
|
|
306
|
+
// With the threads beta, a request without `thread` is a normal request.
|
|
307
|
+
const response = await send(params);
|
|
308
|
+
setThreadState(thread.sessionId, "off");
|
|
309
|
+
return { response, thread: undefined };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Records where the session's next request can continue: after `output`, the clean reply to the
|
|
314
|
+
* thread request `thread`, whose full request was `params`.
|
|
315
|
+
*/
|
|
316
|
+
function recordThreadReply(model, thread, params, output, isOAuth, allowEmptySignature) {
|
|
317
|
+
// `responseModel` is set only when a fallback model answered; the server does not store those
|
|
318
|
+
// replies, so they cannot be continued.
|
|
319
|
+
if (output.responseId === undefined || output.responseModel !== undefined)
|
|
320
|
+
return;
|
|
321
|
+
// Convert the reply like the next request will, keeping only the assistant message
|
|
322
|
+
// (transformMessages adds placeholder results for trailing tool calls).
|
|
323
|
+
const reply = convertMessages(transformMessages([output], model, normalizeToolCallId), isOAuth, undefined, allowEmptySignature).messages.filter((message) => message.role === "assistant");
|
|
324
|
+
const prefix = [...params.messages, ...reply];
|
|
325
|
+
setThreadState(thread.sessionId, {
|
|
326
|
+
prefixCount: prefix.length,
|
|
327
|
+
prefixHash: hashThreadPrefix(model, params, prefix),
|
|
328
|
+
replyId: output.responseId,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
156
331
|
function mergeHeaders(...headerSources) {
|
|
157
332
|
const merged = {};
|
|
158
333
|
for (const headers of headerSources) {
|
|
@@ -377,7 +552,8 @@ async function* iterateAnthropicEvents(response, signal) {
|
|
|
377
552
|
}
|
|
378
553
|
export const stream = (model, context, options) => {
|
|
379
554
|
const stream = new AssistantMessageEventStream();
|
|
380
|
-
const
|
|
555
|
+
const compat = getAnthropicCompat(model);
|
|
556
|
+
const normalizedContext = resolveTranscript(context, compat.supportsMidConvoSystemMessages);
|
|
381
557
|
const currentTools = getCurrentTools(normalizedContext.messages);
|
|
382
558
|
(async () => {
|
|
383
559
|
const output = {
|
|
@@ -402,6 +578,12 @@ export const stream = (model, context, options) => {
|
|
|
402
578
|
let isOAuth;
|
|
403
579
|
let usageModel = model;
|
|
404
580
|
let inputTransformations;
|
|
581
|
+
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
|
|
582
|
+
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
|
|
583
|
+
// Session whose requests may use threads; undefined when threads don't apply. Sessions where
|
|
584
|
+
// the endpoint rejected threads ("off") still send the threads beta, which the resend that
|
|
585
|
+
// turned them off was accepted with; `prepareThreadRequest()` just sends no `thread`.
|
|
586
|
+
const threadSessionId = compat.supportsMessageThreads ? cacheSessionId : undefined;
|
|
405
587
|
if (options?.client) {
|
|
406
588
|
client = options.client;
|
|
407
589
|
isOAuth = false;
|
|
@@ -419,8 +601,6 @@ export const stream = (model, context, options) => {
|
|
|
419
601
|
hasImages,
|
|
420
602
|
});
|
|
421
603
|
}
|
|
422
|
-
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
|
|
423
|
-
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
|
|
424
604
|
const created = createClient(model, apiKey, options?.headers, options?.fetch, copilotDynamicHeaders, cacheSessionId, federation, options?.identity);
|
|
425
605
|
client = created.client;
|
|
426
606
|
isOAuth = created.isOAuthToken;
|
|
@@ -430,24 +610,38 @@ export const stream = (model, context, options) => {
|
|
|
430
610
|
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
|
431
611
|
maxRetries: 0,
|
|
432
612
|
};
|
|
433
|
-
|
|
613
|
+
const send = (body) => retryProviderRequest(() => client.beta.messages.create(body, requestOptions).asResponse(), {
|
|
614
|
+
maxRetries: options?.maxRetries,
|
|
615
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
616
|
+
signal: options?.signal,
|
|
617
|
+
});
|
|
618
|
+
let disablePreferredStrictTools = hasExceededStrictToolLimits(options?.sessionId);
|
|
434
619
|
let response;
|
|
620
|
+
let params;
|
|
621
|
+
// The thread request that produced the response; undefined when no `thread` was sent.
|
|
622
|
+
let thread;
|
|
435
623
|
for (;;) {
|
|
436
624
|
options?.signal?.throwIfAborted();
|
|
437
|
-
const built = buildParams(model, normalizedContext, isOAuth, options,
|
|
625
|
+
const built = buildParams(model, normalizedContext, isOAuth, options, {
|
|
626
|
+
disablePreferredStrictTools,
|
|
627
|
+
messageThreads: threadSessionId !== undefined,
|
|
628
|
+
});
|
|
438
629
|
if (built.providerThinkingLevel)
|
|
439
630
|
output.providerThinkingLevel = built.providerThinkingLevel;
|
|
440
|
-
|
|
631
|
+
params = built.params;
|
|
441
632
|
const nextParams = await options?.onPayload?.(params, model);
|
|
442
633
|
if (nextParams !== undefined) {
|
|
443
634
|
params = { ...nextParams, stream: true };
|
|
444
635
|
}
|
|
636
|
+
const prepared = threadSessionId !== undefined && options?.disableContinuation !== true
|
|
637
|
+
? prepareThreadRequest(model, threadSessionId, params)
|
|
638
|
+
: undefined;
|
|
445
639
|
try {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
640
|
+
const sent = prepared
|
|
641
|
+
? await sendThreadRequest(prepared, params, send, output, options?.signal)
|
|
642
|
+
: { response: await send(params), thread: undefined };
|
|
643
|
+
response = sent.response;
|
|
644
|
+
thread = sent.thread;
|
|
451
645
|
break;
|
|
452
646
|
}
|
|
453
647
|
catch (error) {
|
|
@@ -462,6 +656,7 @@ export const stream = (model, context, options) => {
|
|
|
462
656
|
throw error;
|
|
463
657
|
}
|
|
464
658
|
disablePreferredStrictTools = true;
|
|
659
|
+
markStrictToolLimitsExceeded(options?.sessionId);
|
|
465
660
|
appendAssistantMessageDiagnostic(output, {
|
|
466
661
|
type: "anthropic_strict_tools_fallback",
|
|
467
662
|
timestamp: Date.now(),
|
|
@@ -683,6 +878,8 @@ export const stream = (model, context, options) => {
|
|
|
683
878
|
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
684
879
|
throw new Error(output.errorMessage || "An unknown error occurred");
|
|
685
880
|
}
|
|
881
|
+
if (thread)
|
|
882
|
+
recordThreadReply(model, thread, params, output, isOAuth, compat.allowEmptySignature);
|
|
686
883
|
if (inputTransformations && inputTransformations.length > 0) {
|
|
687
884
|
appendAssistantMessageDiagnostic(output, {
|
|
688
885
|
type: "anthropic_input_transformations",
|
|
@@ -701,7 +898,7 @@ export const stream = (model, context, options) => {
|
|
|
701
898
|
}
|
|
702
899
|
catch (error) {
|
|
703
900
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
704
|
-
output.errorMessage = error instanceof Error ? error
|
|
901
|
+
output.errorMessage = error instanceof Error ? withCauseDetail(error) : JSON.stringify(error);
|
|
705
902
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
706
903
|
stream.end();
|
|
707
904
|
}
|
|
@@ -836,7 +1033,7 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
|
|
|
836
1033
|
});
|
|
837
1034
|
return { client, isOAuthToken: false };
|
|
838
1035
|
}
|
|
839
|
-
function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges,
|
|
1036
|
+
function getBetaFeatures(model, context, options, { isOAuthToken, nativeToolChanges, messageThreads }) {
|
|
840
1037
|
let configuredFeatures;
|
|
841
1038
|
for (const headers of [model.headers, options?.headers]) {
|
|
842
1039
|
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
@@ -872,9 +1069,11 @@ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, option
|
|
|
872
1069
|
}
|
|
873
1070
|
if (nativeToolChanges)
|
|
874
1071
|
features.push(INLINE_TOOLS_BETA);
|
|
1072
|
+
if (messageThreads)
|
|
1073
|
+
features.push(MESSAGE_THREADS_BETA);
|
|
875
1074
|
return [...new Set(features)];
|
|
876
1075
|
}
|
|
877
|
-
function buildParams(model, context, isOAuthToken, options,
|
|
1076
|
+
function buildParams(model, context, isOAuthToken, options, flags) {
|
|
878
1077
|
const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);
|
|
879
1078
|
const compat = getAnthropicCompat(model);
|
|
880
1079
|
const initialSystemMessage = getInitialSystemMessage(context.messages);
|
|
@@ -887,7 +1086,7 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
|
|
|
887
1086
|
// active tool to anchor the placeholder. Otherwise the current tool list is sent.
|
|
888
1087
|
let hasPreferredStrictTools = false;
|
|
889
1088
|
const convertToolDefinitions = (tools, toolCacheControl) => {
|
|
890
|
-
const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, disablePreferredStrictTools);
|
|
1089
|
+
const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, flags.disablePreferredStrictTools);
|
|
891
1090
|
if (definitions.some((definition, index) => definition.strict === true && prefersJsonSchemaStrictSampling(tools[index]))) {
|
|
892
1091
|
hasPreferredStrictTools = true;
|
|
893
1092
|
}
|
|
@@ -900,7 +1099,11 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
|
|
|
900
1099
|
const effortMessages = model.compat?.supportsMidConvoEffort === true && !thinkingDisabled;
|
|
901
1100
|
const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, effortMessages ? model.provider : undefined, nativeToolChanges ? convertToolDefinitions : undefined);
|
|
902
1101
|
const activeEffort = options?.effort ?? "high";
|
|
903
|
-
const betaFeatures = getBetaFeatures(model, context,
|
|
1102
|
+
const betaFeatures = getBetaFeatures(model, context, options, {
|
|
1103
|
+
isOAuthToken,
|
|
1104
|
+
nativeToolChanges,
|
|
1105
|
+
messageThreads: flags.messageThreads,
|
|
1106
|
+
});
|
|
904
1107
|
const params = {
|
|
905
1108
|
model: model.id,
|
|
906
1109
|
messages: effortMessages ? insertThinkingLevelMessages(converted, activeEffort) : converted.messages,
|
|
@@ -1293,10 +1496,14 @@ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supp
|
|
|
1293
1496
|
: resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
|
|
1294
1497
|
const parameters = getJsonSchemaToolParameters(tool, strict);
|
|
1295
1498
|
const schema = parameters;
|
|
1499
|
+
// Keep shared definitions so property $refs still resolve. Other root keywords stay out:
|
|
1500
|
+
// Anthropic rejects root anyOf/oneOf/allOf with a 400.
|
|
1296
1501
|
const legacyInputSchema = {
|
|
1297
1502
|
type: "object",
|
|
1298
1503
|
properties: schema.properties ?? {},
|
|
1299
1504
|
required: schema.required ?? [],
|
|
1505
|
+
...(schema.$defs === undefined ? {} : { $defs: schema.$defs }),
|
|
1506
|
+
...(schema.definitions === undefined ? {} : { definitions: schema.definitions }),
|
|
1300
1507
|
};
|
|
1301
1508
|
const inputSchema = strict === true
|
|
1302
1509
|
? {
|