@fractaal/pi-ai 1.4.1 → 1.6.0
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 +50 -34
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +224 -27
- 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 +54 -25
- 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 +4 -0
- package/dist/api/constrained-sampling.d.ts.map +1 -1
- package/dist/api/constrained-sampling.js +18 -0
- 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 +10 -3
- 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/pi-messages.d.ts.map +1 -1
- package/dist/api/pi-messages.js +9 -0
- package/dist/api/pi-messages.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/auth/oauth/github-copilot.d.ts.map +1 -1
- package/dist/auth/oauth/github-copilot.js +4 -11
- package/dist/auth/oauth/github-copilot.js.map +1 -1
- package/dist/auth/oauth/kimi-coding.d.ts.map +1 -1
- package/dist/auth/oauth/kimi-coding.js +4 -22
- package/dist/auth/oauth/kimi-coding.js.map +1 -1
- 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/auth/oauth/meta.d.ts.map +1 -1
- package/dist/auth/oauth/meta.js +1 -14
- package/dist/auth/oauth/meta.js.map +1 -1
- package/dist/auth/oauth/radius.d.ts.map +1 -1
- package/dist/auth/oauth/radius.js +9 -3
- package/dist/auth/oauth/radius.js.map +1 -1
- package/dist/auth/oauth/trusted-url.d.ts +7 -0
- package/dist/auth/oauth/trusted-url.d.ts.map +1 -0
- package/dist/auth/oauth/trusted-url.js +12 -0
- package/dist/auth/oauth/trusted-url.js.map +1 -0
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- 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 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- 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.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 +5 -1
- package/dist/models.generated.js.map +1 -1
- package/dist/models.js +8 -8
- 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/amazon-bedrock.models.js +1 -1
- package/dist/providers/amazon-bedrock.models.js.map +1 -1
- package/dist/providers/ant-ling.models.js +1 -1
- package/dist/providers/ant-ling.models.js.map +1 -1
- package/dist/providers/anthropic.models.js +1 -1
- package/dist/providers/anthropic.models.js.map +1 -1
- package/dist/providers/azure.models.js +1 -1
- package/dist/providers/azure.models.js.map +1 -1
- package/dist/providers/baseten.models.js +1 -1
- package/dist/providers/baseten.models.js.map +1 -1
- package/dist/providers/cerebras.models.js +1 -1
- package/dist/providers/cerebras.models.js.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.models.js +1 -1
- package/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
- package/dist/providers/cloudflare-workers-ai.models.js +1 -1
- package/dist/providers/cloudflare-workers-ai.models.js.map +1 -1
- package/dist/providers/compat-schema.d.ts +441 -456
- package/dist/providers/compat-schema.d.ts.map +1 -1
- package/dist/providers/compat-schema.js +163 -112
- 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/cloudflare-workers-ai.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-vertex.json +1 -1
- package/dist/providers/data/google.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/kimi-coding.json +1 -1
- package/dist/providers/data/mistral.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/openai-codex.json +1 -1
- package/dist/providers/data/openai.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-cn.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/qwen-token-plan.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/data/xai.json +1 -1
- package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
- package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
- package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
- package/dist/providers/data/xiaomi.json +1 -1
- package/dist/providers/data/zai-coding-cn.json +1 -1
- package/dist/providers/data/zai.json +1 -1
- package/dist/providers/deepseek.models.js +1 -1
- package/dist/providers/deepseek.models.js.map +1 -1
- package/dist/providers/fireworks.models.js +1 -1
- package/dist/providers/fireworks.models.js.map +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/github-copilot.models.js +1 -1
- package/dist/providers/github-copilot.models.js.map +1 -1
- package/dist/providers/google-vertex.models.js +1 -1
- package/dist/providers/google-vertex.models.js.map +1 -1
- package/dist/providers/google.models.js +1 -1
- package/dist/providers/google.models.js.map +1 -1
- package/dist/providers/groq.models.js +1 -1
- package/dist/providers/groq.models.js.map +1 -1
- package/dist/providers/huggingface.models.js +1 -1
- package/dist/providers/huggingface.models.js.map +1 -1
- package/dist/providers/kimi-coding.models.js +1 -1
- package/dist/providers/kimi-coding.models.js.map +1 -1
- package/dist/providers/meta.models.js +1 -1
- package/dist/providers/meta.models.js.map +1 -1
- package/dist/providers/minimax-cn.models.js +1 -1
- package/dist/providers/minimax-cn.models.js.map +1 -1
- package/dist/providers/minimax.models.js +1 -1
- package/dist/providers/minimax.models.js.map +1 -1
- package/dist/providers/mistral.models.js +1 -1
- package/dist/providers/mistral.models.js.map +1 -1
- package/dist/providers/model-schema.d.ts +65 -65
- package/dist/providers/model-schema.d.ts.map +1 -1
- package/dist/providers/model-schema.js +41 -30
- package/dist/providers/model-schema.js.map +1 -1
- package/dist/providers/moonshotai-cn.models.js +1 -1
- package/dist/providers/moonshotai-cn.models.js.map +1 -1
- package/dist/providers/moonshotai.models.js +1 -1
- package/dist/providers/moonshotai.models.js.map +1 -1
- package/dist/providers/nvidia.models.js +1 -1
- package/dist/providers/nvidia.models.js.map +1 -1
- package/dist/providers/openai-codex-catalog.d.ts.map +1 -1
- package/dist/providers/openai-codex-catalog.js +44 -44
- package/dist/providers/openai-codex-catalog.js.map +1 -1
- package/dist/providers/openai-codex.models.js +1 -1
- package/dist/providers/openai-codex.models.js.map +1 -1
- package/dist/providers/openai.models.js +1 -1
- package/dist/providers/openai.models.js.map +1 -1
- package/dist/providers/opencode-go.models.js +1 -1
- package/dist/providers/opencode-go.models.js.map +1 -1
- package/dist/providers/opencode.js +1 -1
- package/dist/providers/opencode.js.map +1 -1
- package/dist/providers/opencode.models.js +1 -1
- package/dist/providers/opencode.models.js.map +1 -1
- package/dist/providers/openrouter.models.js +1 -1
- package/dist/providers/openrouter.models.js.map +1 -1
- package/dist/providers/qwen-token-plan-cn.models.js +1 -1
- package/dist/providers/qwen-token-plan-cn.models.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/providers/qwen-token-plan-individual.models.js +1 -1
- package/dist/providers/qwen-token-plan-individual.models.js.map +1 -1
- package/dist/providers/qwen-token-plan.models.js +1 -1
- package/dist/providers/qwen-token-plan.models.js.map +1 -1
- package/dist/providers/radius.models.js +1 -1
- package/dist/providers/radius.models.js.map +1 -1
- package/dist/providers/schema-builders.d.ts +10 -0
- package/dist/providers/schema-builders.d.ts.map +1 -0
- package/dist/providers/schema-builders.js +18 -0
- package/dist/providers/schema-builders.js.map +1 -0
- package/dist/providers/together.models.js +1 -1
- package/dist/providers/together.models.js.map +1 -1
- package/dist/providers/typesafe.models.js +1 -1
- package/dist/providers/typesafe.models.js.map +1 -1
- package/dist/providers/vercel-ai-gateway.models.js +1 -1
- package/dist/providers/vercel-ai-gateway.models.js.map +1 -1
- package/dist/providers/xai.models.js +1 -1
- package/dist/providers/xai.models.js.map +1 -1
- package/dist/providers/xiaomi-token-plan-ams.models.js +1 -1
- package/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
- package/dist/providers/xiaomi-token-plan-cn.models.js +1 -1
- package/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
- package/dist/providers/xiaomi-token-plan-sgp.models.js +1 -1
- package/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
- package/dist/providers/xiaomi.models.js +1 -1
- package/dist/providers/xiaomi.models.js.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.js +1 -1
- package/dist/providers/zai.models.js.map +1 -1
- package/dist/types.d.ts +13 -3
- 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/overflow.d.ts.map +1 -1
- package/dist/utils/overflow.js +2 -0
- package/dist/utils/overflow.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/{typebox-helpers.d.ts → string-enum.d.ts} +5 -5
- package/dist/utils/string-enum.d.ts.map +1 -0
- package/dist/utils/{typebox-helpers.js → string-enum.js} +5 -6
- package/dist/utils/string-enum.js.map +1 -0
- package/dist/utils/transcript.d.ts +1 -1
- package/dist/utils/transcript.js +1 -1
- package/dist/utils/transcript.js.map +1 -1
- package/dist/utils/validation.d.ts +5 -3
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +25 -446
- package/dist/utils/validation.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 +18 -9
- package/dist/utils/typebox-helpers.d.ts.map +0 -1
- package/dist/utils/typebox-helpers.js.map +0 -1
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**
|
|
@@ -103,15 +103,16 @@ Unified LLM API with provider collections, automatic auth resolution, token and
|
|
|
103
103
|
npm install @earendil-works/pi-ai
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Tool parameters are JSON Schema. The examples build them with `@earendil-works/pi-schema`, which also types the arguments (`npm install @earendil-works/pi-schema`); TypeBox schemas and plain JSON Schema objects work as well.
|
|
107
107
|
|
|
108
108
|
## Quick Start
|
|
109
109
|
|
|
110
110
|
You build a `Models` collection of providers and stream through it. The quickest start registers every built-in provider; apps that care about bundle size register individual providers instead (see [Provider Factories](#provider-factories) and [Bundling and Tree Shaking](#bundling-and-tree-shaking)).
|
|
111
111
|
|
|
112
112
|
```typescript
|
|
113
|
-
import {
|
|
113
|
+
import { type Context, type Tool } from '@earendil-works/pi-ai';
|
|
114
114
|
import { builtinModels } from '@earendil-works/pi-ai/providers/all';
|
|
115
|
+
import { obj, opt, str } from '@earendil-works/pi-schema';
|
|
115
116
|
|
|
116
117
|
// A Models collection with every built-in provider registered
|
|
117
118
|
const models = builtinModels();
|
|
@@ -119,12 +120,12 @@ const models = builtinModels();
|
|
|
119
120
|
// Sync lookup against the collection
|
|
120
121
|
const model = models.getModel('openai', 'gpt-4o-mini')!;
|
|
121
122
|
|
|
122
|
-
// Define tools with
|
|
123
|
+
// Define tools with JSON Schema; pi-schema's builders also type the arguments
|
|
123
124
|
const tools: Tool[] = [{
|
|
124
125
|
name: 'get_time',
|
|
125
126
|
description: 'Get the current time',
|
|
126
|
-
parameters:
|
|
127
|
-
timezone:
|
|
127
|
+
parameters: obj({
|
|
128
|
+
timezone: opt(str({ description: 'Optional timezone (e.g., America/New_York)' }))
|
|
128
129
|
})
|
|
129
130
|
}];
|
|
130
131
|
|
|
@@ -523,6 +524,7 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
|
|
|
523
524
|
| Qwen Token Plan (existing catalog) | `QWEN_TOKEN_PLAN_API_KEY` |
|
|
524
525
|
| Qwen Token Plan (Individual) | `QWEN_TOKEN_PLAN_API_KEY` |
|
|
525
526
|
| Qwen Token Plan (China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |
|
|
527
|
+
| Qwen Token Plan (Individual, China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |
|
|
526
528
|
| Xiaomi MiMo (API billing) | `XIAOMI_API_KEY` |
|
|
527
529
|
| Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` |
|
|
528
530
|
| Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` |
|
|
@@ -532,40 +534,43 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
|
|
|
532
534
|
`qwen-token-plan-individual` and `qwen-token-plan` share the international endpoint and
|
|
533
535
|
`QWEN_TOKEN_PLAN_API_KEY`. The Individual provider exposes only the models documented for Individual
|
|
534
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`.
|
|
535
539
|
Stored credentials remain provider-scoped, so save the key under the provider ID you register.
|
|
536
540
|
|
|
537
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.
|
|
538
542
|
|
|
539
543
|
## Tools
|
|
540
544
|
|
|
541
|
-
Tools enable LLMs to interact with external systems.
|
|
545
|
+
Tools enable LLMs to interact with external systems. A tool's `parameters` is a JSON Schema object: built with `@earendil-works/pi-schema`'s builders (which also type the arguments through its `Static`), with TypeBox, or written by hand. `validateToolCall()` and `validateToolArguments()` validate with pi-schema, coercing loosely typed arguments first (numeric strings, `"true"`, a single value for an array, `null` for an omitted optional property). Schemas are plain JSON, so they serialize with the rest of a context.
|
|
542
546
|
|
|
543
547
|
### Defining Tools
|
|
544
548
|
|
|
545
549
|
```typescript
|
|
546
|
-
import {
|
|
550
|
+
import { type Tool, StringEnum } from '@earendil-works/pi-ai';
|
|
551
|
+
import { arr, obj, str } from '@earendil-works/pi-schema';
|
|
547
552
|
|
|
548
|
-
// Define tool parameters with
|
|
553
|
+
// Define tool parameters with pi-schema's builders
|
|
549
554
|
const weatherTool: Tool = {
|
|
550
555
|
name: 'get_weather',
|
|
551
556
|
description: 'Get current weather for a location',
|
|
552
|
-
parameters:
|
|
553
|
-
location:
|
|
557
|
+
parameters: obj({
|
|
558
|
+
location: str({ description: 'City name or coordinates' }),
|
|
554
559
|
units: StringEnum(['celsius', 'fahrenheit'], { default: 'celsius' })
|
|
555
560
|
})
|
|
556
561
|
};
|
|
557
562
|
|
|
558
|
-
// Note:
|
|
559
|
-
//
|
|
563
|
+
// Note: StringEnum gives { type: "string", enum: [...] }, which every provider accepts,
|
|
564
|
+
// including Google's, which rejects anyOf/const patterns
|
|
560
565
|
|
|
561
566
|
const bookMeetingTool: Tool = {
|
|
562
567
|
name: 'book_meeting',
|
|
563
568
|
description: 'Schedule a meeting',
|
|
564
|
-
parameters:
|
|
565
|
-
title:
|
|
566
|
-
startTime:
|
|
567
|
-
endTime:
|
|
568
|
-
attendees:
|
|
569
|
+
parameters: obj({
|
|
570
|
+
title: str({ minLength: 1 }),
|
|
571
|
+
startTime: str({ format: 'date-time' }),
|
|
572
|
+
endTime: str({ format: 'date-time' }),
|
|
573
|
+
attendees: arr(str({ format: 'email' }), { minItems: 1 })
|
|
569
574
|
})
|
|
570
575
|
};
|
|
571
576
|
```
|
|
@@ -578,10 +583,7 @@ Tools can opt in to provider-side constrained sampling. For JSON-schema tools, `
|
|
|
578
583
|
const strictTool: Tool = {
|
|
579
584
|
name: 'edit_file',
|
|
580
585
|
description: 'Edit a file',
|
|
581
|
-
parameters:
|
|
582
|
-
path: Type.String(),
|
|
583
|
-
content: Type.String()
|
|
584
|
-
}, { additionalProperties: false }),
|
|
586
|
+
parameters: obj({ path: str(), content: str() }), // obj() is closed: additionalProperties: false
|
|
585
587
|
constrainedSampling: { type: 'json_schema', strict: 'prefer' }
|
|
586
588
|
};
|
|
587
589
|
```
|
|
@@ -592,9 +594,7 @@ Strict JSON-schema constrained sampling is supported for OpenAI, Anthropic, supp
|
|
|
592
594
|
const patchTool: Tool = {
|
|
593
595
|
name: 'apply_patch',
|
|
594
596
|
description: 'Apply a patch',
|
|
595
|
-
parameters:
|
|
596
|
-
input: Type.String()
|
|
597
|
-
}, { additionalProperties: false }),
|
|
597
|
+
parameters: obj({ input: str() }),
|
|
598
598
|
constrainedSampling: {
|
|
599
599
|
type: 'grammar',
|
|
600
600
|
variants: {
|
|
@@ -698,7 +698,7 @@ for await (const event of s) {
|
|
|
698
698
|
|
|
699
699
|
### Validating Tool Arguments
|
|
700
700
|
|
|
701
|
-
When implementing your own tool execution loop, use `validateToolCall` to validate arguments before passing them to your tools:
|
|
701
|
+
When implementing your own tool execution loop, use `validateToolCall` to validate arguments before passing them to your tools. It coerces loosely typed arguments with pi-schema (`compile(schema, { coerce: true }).parse()`), returns the coerced arguments, and throws an error listing pi-schema's errors (`path: message`) followed by the arguments it received:
|
|
702
702
|
|
|
703
703
|
```typescript
|
|
704
704
|
import { validateToolCall, type Tool } from '@earendil-works/pi-ai';
|
|
@@ -894,7 +894,7 @@ console.log(model.output); // ['image'] or ['image', 'text']
|
|
|
894
894
|
|
|
895
895
|
## Classification
|
|
896
896
|
|
|
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,
|
|
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:
|
|
898
898
|
|
|
899
899
|
| Provider | Model IDs | Auth |
|
|
900
900
|
| --- | --- | --- |
|
|
@@ -904,6 +904,7 @@ Classifier models consume structured JSON state, and optionally images, and answ
|
|
|
904
904
|
| `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
|
|
905
905
|
| `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
|
|
906
906
|
| `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |
|
|
907
|
+
| `github-copilot` | `microsoft-decision-1` | `COPILOT_GITHUB_TOKEN` or GitHub Copilot OAuth |
|
|
907
908
|
|
|
908
909
|
```typescript
|
|
909
910
|
import { builtinModels } from '@earendil-works/pi-ai/providers/all';
|
|
@@ -1440,6 +1441,8 @@ Built-in API implementations live under `./api/<api-id>`:
|
|
|
1440
1441
|
|
|
1441
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>`.
|
|
1442
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
|
+
|
|
1443
1446
|
### OpenAI Compatibility Settings
|
|
1444
1447
|
|
|
1445
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.
|
|
@@ -1459,6 +1462,7 @@ interface OpenAICompletionsCompat {
|
|
|
1459
1462
|
maxTokensField?: 'max_completion_tokens' | 'max_tokens'; // Which field name to use (default: max_completion_tokens)
|
|
1460
1463
|
requiresToolResultName?: boolean; // Whether tool results require the `name` field (default: false)
|
|
1461
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)
|
|
1462
1466
|
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
|
|
1463
1467
|
requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
|
|
1464
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)
|
|
@@ -1726,8 +1730,8 @@ models.setProvider(openaiProvider());
|
|
|
1726
1730
|
|
|
1727
1731
|
Rules:
|
|
1728
1732
|
|
|
1729
|
-
- `@earendil-works/pi-ai/models` exports the model runtime (`createModels`, `createProvider`, model helpers, and their types) without
|
|
1730
|
-
- `@earendil-works/pi-ai` is the core entrypoint and does not import built-in catalogs, real provider factories, or SDK implementations, but it eagerly imports
|
|
1733
|
+
- `@earendil-works/pi-ai/models` exports the model runtime (`createModels`, `createProvider`, model helpers, and their types) without schema validation, built-in catalogs, or SDK implementations. Other types can still use `import type` from the root.
|
|
1734
|
+
- `@earendil-works/pi-ai` is the core entrypoint and does not import built-in catalogs, real provider factories, or SDK implementations, but it eagerly imports schema validation (pi-schema). Unbundled Node scripts do not tree-shake its unused exports; prefer `./models`, `./providers/faux`, and specific `./utils/*` subpaths when those are all you need.
|
|
1731
1735
|
- `@earendil-works/pi-ai/providers/<provider>` imports that provider's catalog and lazy API wrapper only.
|
|
1732
1736
|
- `@earendil-works/pi-ai/providers/all` imports every built-in provider factory and all catalogs. Use it only when you want the full built-in set.
|
|
1733
1737
|
- With code splitting, provider SDKs stay in lazy chunks and load on first request.
|
|
@@ -1767,6 +1771,18 @@ setBedrockProviderModule(bedrockProviderModule);
|
|
|
1767
1771
|
|
|
1768
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.
|
|
1769
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
|
+
|
|
1770
1786
|
### Provider-Scoped Environment Overrides
|
|
1771
1787
|
|
|
1772
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`.
|
|
@@ -1792,7 +1808,7 @@ Several providers support OAuth authentication instead of static API keys:
|
|
|
1792
1808
|
|
|
1793
1809
|
- **Anthropic** (Claude Pro/Max subscription)
|
|
1794
1810
|
- **OpenAI** (Sign in with ChatGPT: uses the ChatGPT subscription with the OpenAI API)
|
|
1795
|
-
- **OpenAI Codex
|
|
1811
|
+
- **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)
|
|
1796
1812
|
- **GitHub Copilot** (Copilot subscription)
|
|
1797
1813
|
- **OpenRouter** (OAuth PKCE that mints a user-controlled API key)
|
|
1798
1814
|
|
|
@@ -1872,7 +1888,7 @@ Built-in login and refresh flows are private provider implementations. Use provi
|
|
|
1872
1888
|
|
|
1873
1889
|
Provider notes:
|
|
1874
1890
|
|
|
1875
|
-
**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.
|
|
1876
1892
|
|
|
1877
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.
|
|
1878
1894
|
|
|
@@ -1928,12 +1944,12 @@ Create a new API implementation file (for example `bedrock-converse-stream.ts`)
|
|
|
1928
1944
|
|
|
1929
1945
|
Add a lazy wrapper `src/api/<api-id>.lazy.ts` (`<name>Api()` via `lazyApi()`) so providers can reference the implementation without importing its SDK. Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai`.
|
|
1930
1946
|
|
|
1931
|
-
#### 3. Model
|
|
1947
|
+
#### 3. Model Catalog (`catalog/providers/<id>.ts`)
|
|
1932
1948
|
|
|
1933
|
-
- Add
|
|
1949
|
+
- Add a provider definition and register it in `catalog/providers/index.ts`; see `catalog/README.md` for the build order and helpers
|
|
1934
1950
|
- Map chat/tool-capable provider data to `Model`, image-generation data to `ImageModel`, and models.dev `type: "decision"` entries to `ClassifierModel`; hydration groups the ignored `src/providers/data/<id>.json` values by API while stable `src/providers/<id>.models.ts` wrappers derive exact model/API types directly from those JSON keys
|
|
1935
1951
|
- Keep model ids unique within each provider and model type; emit separate entries when an upstream model supports multiple operations
|
|
1936
|
-
- Handle provider-specific quirks (pricing format, capability flags, model ID transformations)
|
|
1952
|
+
- Handle provider-specific quirks (pricing format, capability flags, model ID transformations) in the provider definition
|
|
1937
1953
|
|
|
1938
1954
|
#### 4. Provider Factory (`src/providers/<id>.ts`)
|
|
1939
1955
|
|
|
@@ -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;AAiKrB,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,CAgXzE,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,7 +16,7 @@ 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, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
|
|
19
|
+
import { getJsonSchemaToolParameters, isStrictToolLimitErrorMessage, 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";
|
|
19
22
|
import { splitErrorToolResultImages, transformMessages } from "./transform-messages.js";
|
|
@@ -122,6 +125,7 @@ const SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-07-01";
|
|
|
122
125
|
const MID_CONVERSATION_OUTPUT_CONFIG_BETA = "mid-conversation-output-config-2026-07-01";
|
|
123
126
|
const THINKING_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
|
|
124
127
|
const INLINE_TOOLS_BETA = "inline-tools-2026-09-15";
|
|
128
|
+
const MESSAGE_THREADS_BETA = "message-threads-2026-08-12";
|
|
125
129
|
/**
|
|
126
130
|
* Stable deferred tool declared whenever native tool changes are in use. Anthropic adds
|
|
127
131
|
* hidden prompt scaffolding for mid-conversation tool changes; declaring this placeholder
|
|
@@ -151,8 +155,178 @@ function getAnthropicCompat(model) {
|
|
|
151
155
|
supportsStrictTools: model.compat?.supportsStrictTools ?? false,
|
|
152
156
|
supportsMidConvoSystemMessages: model.compat?.supportsMidConvoSystemMessages ?? false,
|
|
153
157
|
supportsMidConvoToolChanges: model.compat?.supportsMidConvoToolChanges ?? false,
|
|
158
|
+
// Gateways that drop unknown request fields would silently turn a `continue` into a request
|
|
159
|
+
// without the earlier conversation, so the default is limited to Anthropic's own endpoint.
|
|
160
|
+
// Widen or remove this restriction once proxies generally forward the `thread` field.
|
|
161
|
+
supportsMessageThreads: model.compat?.supportsMessageThreads ??
|
|
162
|
+
(model.provider === "anthropic" && model.baseUrl.includes("api.anthropic.com")),
|
|
154
163
|
};
|
|
155
164
|
}
|
|
165
|
+
/** Per session: where the next request can continue, or "off" after the endpoint rejected threads. */
|
|
166
|
+
const threadState = new Map();
|
|
167
|
+
const MAX_THREAD_STATE_ENTRIES = 10_000;
|
|
168
|
+
/** Rejections of a `continue` that a new thread fixes: expired, raced, or a fixed setting changed. */
|
|
169
|
+
const NEW_THREAD_ERRORS = new Set([
|
|
170
|
+
"thread_not_found",
|
|
171
|
+
"thread_already_continued",
|
|
172
|
+
"thread_continue_in_flight",
|
|
173
|
+
"thread_fingerprint_mismatch",
|
|
174
|
+
]);
|
|
175
|
+
/**
|
|
176
|
+
* Sets a session's thread state, keeping at most `MAX_THREAD_STATE_ENTRIES` sessions.
|
|
177
|
+
*
|
|
178
|
+
* State is normally freed by `cleanupSessionResources(sessionId)` when a session ends, as pi's
|
|
179
|
+
* coding agent does. The cap bounds memory for library users that never call it, for example a
|
|
180
|
+
* server passing a new `sessionId` per conversation. Dropping the least recently updated session
|
|
181
|
+
* is cheap: its thread has most likely expired on the server already (after a few minutes idle),
|
|
182
|
+
* and otherwise its next request just starts a new thread.
|
|
183
|
+
*/
|
|
184
|
+
function setThreadState(sessionId, value) {
|
|
185
|
+
// Re-insert so Map order stays oldest-first for the cap.
|
|
186
|
+
threadState.delete(sessionId);
|
|
187
|
+
threadState.set(sessionId, value);
|
|
188
|
+
if (threadState.size > MAX_THREAD_STATE_ENTRIES) {
|
|
189
|
+
const oldest = threadState.keys().next().value;
|
|
190
|
+
if (oldest !== undefined)
|
|
191
|
+
threadState.delete(oldest);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
registerSessionResourceCleanup((sessionId) => {
|
|
195
|
+
if (sessionId)
|
|
196
|
+
threadState.delete(sessionId);
|
|
197
|
+
else
|
|
198
|
+
threadState.clear();
|
|
199
|
+
});
|
|
200
|
+
/**
|
|
201
|
+
* Hash of everything a `continue` builds on: the settings that must stay the same within a thread
|
|
202
|
+
* and the messages the server already holds.
|
|
203
|
+
*
|
|
204
|
+
* - Settings, as sent: the endpoint (a thread exists on one endpoint only), and the fixed request
|
|
205
|
+
* fields. A `continue` omits `system` and `tools` and the server keeps the thread's own, so only
|
|
206
|
+
* this check can notice a change to them, including their cache markers. The server rejects
|
|
207
|
+
* changes to the other fields with `thread_fingerprint_mismatch`; checking them here saves that
|
|
208
|
+
* rejected request. A fixed field missing here costs only that rejection, which also starts a
|
|
209
|
+
* new thread.
|
|
210
|
+
* - Messages, normalized so messages that mean the same to the API hash the same.
|
|
211
|
+
*/
|
|
212
|
+
function hashThreadPrefix(model, params, messages) {
|
|
213
|
+
const { system, tools, thinking, output_config, tool_choice, betas, fallbacks } = params;
|
|
214
|
+
const settings = JSON.stringify([
|
|
215
|
+
model.provider,
|
|
216
|
+
model.baseUrl,
|
|
217
|
+
params.model,
|
|
218
|
+
system,
|
|
219
|
+
tools,
|
|
220
|
+
thinking,
|
|
221
|
+
output_config,
|
|
222
|
+
tool_choice,
|
|
223
|
+
betas,
|
|
224
|
+
fallbacks,
|
|
225
|
+
]);
|
|
226
|
+
const conversation = JSON.stringify(messages, (key, value) => {
|
|
227
|
+
// Cache markers don't change what a message means, but they move: pi puts one on the newest
|
|
228
|
+
// message, so the message that had it in the last request has none in this one. Extensions
|
|
229
|
+
// may add markers anywhere too.
|
|
230
|
+
if (key === "cache_control")
|
|
231
|
+
return undefined;
|
|
232
|
+
// To attach that marker, the newest user message's string content is sent as one text block;
|
|
233
|
+
// the next request sends the same message as a string: "hi" vs [{ type: "text", text: "hi" }].
|
|
234
|
+
if (key === "content" && typeof value === "string")
|
|
235
|
+
return [{ type: "text", text: value }];
|
|
236
|
+
return value;
|
|
237
|
+
});
|
|
238
|
+
return shortHash(settings + conversation);
|
|
239
|
+
}
|
|
240
|
+
function createThreadRequest(sessionId, params) {
|
|
241
|
+
return { sessionId, body: { ...params, thread: { type: "create" } } };
|
|
242
|
+
}
|
|
243
|
+
function prepareThreadRequest(model, sessionId, params) {
|
|
244
|
+
const state = threadState.get(sessionId);
|
|
245
|
+
if (state === "off")
|
|
246
|
+
return undefined;
|
|
247
|
+
if (state !== undefined &&
|
|
248
|
+
params.messages.length > state.prefixCount &&
|
|
249
|
+
hashThreadPrefix(model, params, params.messages.slice(0, state.prefixCount)) === state.prefixHash) {
|
|
250
|
+
// Each reply can be continued once; deleting also keeps a concurrent request from trying.
|
|
251
|
+
threadState.delete(sessionId);
|
|
252
|
+
const { system: _system, tools: _tools, ...rest } = params;
|
|
253
|
+
return {
|
|
254
|
+
body: {
|
|
255
|
+
...rest,
|
|
256
|
+
messages: params.messages.slice(state.prefixCount),
|
|
257
|
+
thread: { type: "continue", previous_message_id: state.replyId },
|
|
258
|
+
},
|
|
259
|
+
sessionId,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return createThreadRequest(sessionId, params);
|
|
263
|
+
}
|
|
264
|
+
/** The `error_code` Anthropic puts in `error.details` of thread rejections. */
|
|
265
|
+
function getThreadErrorCode(error) {
|
|
266
|
+
const body = error.error;
|
|
267
|
+
const code = body?.error?.details?.error_code;
|
|
268
|
+
return typeof code === "string" ? code : undefined;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Sends a thread request. A rejection about the thread (never a started stream) is resent once in
|
|
272
|
+
* full: as a new thread when the old one can't be continued, or without `thread` when the endpoint
|
|
273
|
+
* can't use threads, which also turns threads off for the session. Other errors are passed on.
|
|
274
|
+
* Returns the response and the thread request that produced it, if any.
|
|
275
|
+
*/
|
|
276
|
+
async function sendThreadRequest(thread, params, send, output, signal) {
|
|
277
|
+
try {
|
|
278
|
+
return { response: await send(thread.body), thread };
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (signal?.aborted || !(error instanceof Anthropic.APIError))
|
|
282
|
+
throw error;
|
|
283
|
+
const errorCode = getThreadErrorCode(error);
|
|
284
|
+
const startNewThread = errorCode !== undefined && NEW_THREAD_ERRORS.has(errorCode);
|
|
285
|
+
// The endpoint can't use threads: it can't thread this request, or it doesn't know the
|
|
286
|
+
// `thread` field and rejects it without a code ("thread: Extra inputs are not permitted"),
|
|
287
|
+
// e.g. with an `anthropic-beta` header that lacks the threads beta.
|
|
288
|
+
const threadsUnsupported = errorCode === "thread_unsupported_request" ||
|
|
289
|
+
(errorCode === undefined && error.status === 400 && error.message.includes("thread"));
|
|
290
|
+
// Any other error (prompt too long, unknown model, strict tool limits, ...) is not about the
|
|
291
|
+
// thread, and resending would only fail again.
|
|
292
|
+
if (!startNewThread && !threadsUnsupported)
|
|
293
|
+
throw error;
|
|
294
|
+
// Recorded before the resend, so a failed resend keeps it.
|
|
295
|
+
appendAssistantMessageDiagnostic(output, {
|
|
296
|
+
type: "anthropic_thread_fallback",
|
|
297
|
+
timestamp: Date.now(),
|
|
298
|
+
error: { message: error.message },
|
|
299
|
+
details: { reason: errorCode ?? String(error.status) },
|
|
300
|
+
});
|
|
301
|
+
if (startNewThread) {
|
|
302
|
+
const created = createThreadRequest(thread.sessionId, params);
|
|
303
|
+
return { response: await send(created.body), thread: created };
|
|
304
|
+
}
|
|
305
|
+
// With the threads beta, a request without `thread` is a normal request.
|
|
306
|
+
const response = await send(params);
|
|
307
|
+
setThreadState(thread.sessionId, "off");
|
|
308
|
+
return { response, thread: undefined };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Records where the session's next request can continue: after `output`, the clean reply to the
|
|
313
|
+
* thread request `thread`, whose full request was `params`.
|
|
314
|
+
*/
|
|
315
|
+
function recordThreadReply(model, thread, params, output, isOAuth, allowEmptySignature) {
|
|
316
|
+
// `responseModel` is set only when a fallback model answered; the server does not store those
|
|
317
|
+
// replies, so they cannot be continued.
|
|
318
|
+
if (output.responseId === undefined || output.responseModel !== undefined)
|
|
319
|
+
return;
|
|
320
|
+
// Convert the reply like the next request will, keeping only the assistant message
|
|
321
|
+
// (transformMessages adds placeholder results for trailing tool calls).
|
|
322
|
+
const reply = convertMessages(transformMessages([output], model, normalizeToolCallId), isOAuth, undefined, allowEmptySignature).messages.filter((message) => message.role === "assistant");
|
|
323
|
+
const prefix = [...params.messages, ...reply];
|
|
324
|
+
setThreadState(thread.sessionId, {
|
|
325
|
+
prefixCount: prefix.length,
|
|
326
|
+
prefixHash: hashThreadPrefix(model, params, prefix),
|
|
327
|
+
replyId: output.responseId,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
156
330
|
function mergeHeaders(...headerSources) {
|
|
157
331
|
const merged = {};
|
|
158
332
|
for (const headers of headerSources) {
|
|
@@ -377,7 +551,8 @@ async function* iterateAnthropicEvents(response, signal) {
|
|
|
377
551
|
}
|
|
378
552
|
export const stream = (model, context, options) => {
|
|
379
553
|
const stream = new AssistantMessageEventStream();
|
|
380
|
-
const
|
|
554
|
+
const compat = getAnthropicCompat(model);
|
|
555
|
+
const normalizedContext = resolveTranscript(context, compat.supportsMidConvoSystemMessages);
|
|
381
556
|
const currentTools = getCurrentTools(normalizedContext.messages);
|
|
382
557
|
(async () => {
|
|
383
558
|
const output = {
|
|
@@ -402,6 +577,12 @@ export const stream = (model, context, options) => {
|
|
|
402
577
|
let isOAuth;
|
|
403
578
|
let usageModel = model;
|
|
404
579
|
let inputTransformations;
|
|
580
|
+
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
|
|
581
|
+
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
|
|
582
|
+
// Session whose requests may use threads; undefined when threads don't apply. Sessions where
|
|
583
|
+
// the endpoint rejected threads ("off") still send the threads beta, which the resend that
|
|
584
|
+
// turned them off was accepted with; `prepareThreadRequest()` just sends no `thread`.
|
|
585
|
+
const threadSessionId = compat.supportsMessageThreads ? cacheSessionId : undefined;
|
|
405
586
|
if (options?.client) {
|
|
406
587
|
client = options.client;
|
|
407
588
|
isOAuth = false;
|
|
@@ -419,8 +600,6 @@ export const stream = (model, context, options) => {
|
|
|
419
600
|
hasImages,
|
|
420
601
|
});
|
|
421
602
|
}
|
|
422
|
-
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
|
|
423
|
-
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
|
|
424
603
|
const created = createClient(model, apiKey, options?.headers, options?.fetch, copilotDynamicHeaders, cacheSessionId, federation, options?.identity);
|
|
425
604
|
client = created.client;
|
|
426
605
|
isOAuth = created.isOAuthToken;
|
|
@@ -430,24 +609,38 @@ export const stream = (model, context, options) => {
|
|
|
430
609
|
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
|
431
610
|
maxRetries: 0,
|
|
432
611
|
};
|
|
612
|
+
const send = (body) => retryProviderRequest(() => client.beta.messages.create(body, requestOptions).asResponse(), {
|
|
613
|
+
maxRetries: options?.maxRetries,
|
|
614
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
615
|
+
signal: options?.signal,
|
|
616
|
+
});
|
|
433
617
|
let disablePreferredStrictTools = false;
|
|
434
618
|
let response;
|
|
619
|
+
let params;
|
|
620
|
+
// The thread request that produced the response; undefined when no `thread` was sent.
|
|
621
|
+
let thread;
|
|
435
622
|
for (;;) {
|
|
436
623
|
options?.signal?.throwIfAborted();
|
|
437
|
-
const built = buildParams(model, normalizedContext, isOAuth, options,
|
|
624
|
+
const built = buildParams(model, normalizedContext, isOAuth, options, {
|
|
625
|
+
disablePreferredStrictTools,
|
|
626
|
+
messageThreads: threadSessionId !== undefined,
|
|
627
|
+
});
|
|
438
628
|
if (built.providerThinkingLevel)
|
|
439
629
|
output.providerThinkingLevel = built.providerThinkingLevel;
|
|
440
|
-
|
|
630
|
+
params = built.params;
|
|
441
631
|
const nextParams = await options?.onPayload?.(params, model);
|
|
442
632
|
if (nextParams !== undefined) {
|
|
443
633
|
params = { ...nextParams, stream: true };
|
|
444
634
|
}
|
|
635
|
+
const prepared = threadSessionId !== undefined && options?.disableContinuation !== true
|
|
636
|
+
? prepareThreadRequest(model, threadSessionId, params)
|
|
637
|
+
: undefined;
|
|
445
638
|
try {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
639
|
+
const sent = prepared
|
|
640
|
+
? await sendThreadRequest(prepared, params, send, output, options?.signal)
|
|
641
|
+
: { response: await send(params), thread: undefined };
|
|
642
|
+
response = sent.response;
|
|
643
|
+
thread = sent.thread;
|
|
451
644
|
break;
|
|
452
645
|
}
|
|
453
646
|
catch (error) {
|
|
@@ -458,11 +651,7 @@ export const stream = (model, context, options) => {
|
|
|
458
651
|
!(error instanceof Anthropic.APIError) ||
|
|
459
652
|
error.status !== 400 ||
|
|
460
653
|
error.type !== "invalid_request_error" ||
|
|
461
|
-
!
|
|
462
|
-
"The compiled grammar is too large",
|
|
463
|
-
"Too many strict tools (",
|
|
464
|
-
"Schemas contains too many parameters with union types (",
|
|
465
|
-
].some((message) => error.message.includes(message))) {
|
|
654
|
+
!isStrictToolLimitErrorMessage(error.message)) {
|
|
466
655
|
throw error;
|
|
467
656
|
}
|
|
468
657
|
disablePreferredStrictTools = true;
|
|
@@ -687,6 +876,8 @@ export const stream = (model, context, options) => {
|
|
|
687
876
|
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
688
877
|
throw new Error(output.errorMessage || "An unknown error occurred");
|
|
689
878
|
}
|
|
879
|
+
if (thread)
|
|
880
|
+
recordThreadReply(model, thread, params, output, isOAuth, compat.allowEmptySignature);
|
|
690
881
|
if (inputTransformations && inputTransformations.length > 0) {
|
|
691
882
|
appendAssistantMessageDiagnostic(output, {
|
|
692
883
|
type: "anthropic_input_transformations",
|
|
@@ -705,7 +896,7 @@ export const stream = (model, context, options) => {
|
|
|
705
896
|
}
|
|
706
897
|
catch (error) {
|
|
707
898
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
708
|
-
output.errorMessage = error instanceof Error ? error
|
|
899
|
+
output.errorMessage = error instanceof Error ? withCauseDetail(error) : JSON.stringify(error);
|
|
709
900
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
710
901
|
stream.end();
|
|
711
902
|
}
|
|
@@ -840,7 +1031,7 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
|
|
|
840
1031
|
});
|
|
841
1032
|
return { client, isOAuthToken: false };
|
|
842
1033
|
}
|
|
843
|
-
function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges,
|
|
1034
|
+
function getBetaFeatures(model, context, options, { isOAuthToken, nativeToolChanges, messageThreads }) {
|
|
844
1035
|
let configuredFeatures;
|
|
845
1036
|
for (const headers of [model.headers, options?.headers]) {
|
|
846
1037
|
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
@@ -876,9 +1067,11 @@ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, option
|
|
|
876
1067
|
}
|
|
877
1068
|
if (nativeToolChanges)
|
|
878
1069
|
features.push(INLINE_TOOLS_BETA);
|
|
1070
|
+
if (messageThreads)
|
|
1071
|
+
features.push(MESSAGE_THREADS_BETA);
|
|
879
1072
|
return [...new Set(features)];
|
|
880
1073
|
}
|
|
881
|
-
function buildParams(model, context, isOAuthToken, options,
|
|
1074
|
+
function buildParams(model, context, isOAuthToken, options, flags) {
|
|
882
1075
|
const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);
|
|
883
1076
|
const compat = getAnthropicCompat(model);
|
|
884
1077
|
const initialSystemMessage = getInitialSystemMessage(context.messages);
|
|
@@ -891,11 +1084,8 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
|
|
|
891
1084
|
// active tool to anchor the placeholder. Otherwise the current tool list is sent.
|
|
892
1085
|
let hasPreferredStrictTools = false;
|
|
893
1086
|
const convertToolDefinitions = (tools, toolCacheControl) => {
|
|
894
|
-
const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, disablePreferredStrictTools);
|
|
895
|
-
if (definitions.some((definition, index) => {
|
|
896
|
-
const config = tools[index].constrainedSampling;
|
|
897
|
-
return definition.strict === true && config && config.type === "json_schema" && config.strict === "prefer";
|
|
898
|
-
})) {
|
|
1087
|
+
const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, flags.disablePreferredStrictTools);
|
|
1088
|
+
if (definitions.some((definition, index) => definition.strict === true && prefersJsonSchemaStrictSampling(tools[index]))) {
|
|
899
1089
|
hasPreferredStrictTools = true;
|
|
900
1090
|
}
|
|
901
1091
|
return definitions;
|
|
@@ -907,7 +1097,11 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
|
|
|
907
1097
|
const effortMessages = model.compat?.supportsMidConvoEffort === true && !thinkingDisabled;
|
|
908
1098
|
const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, effortMessages ? model.provider : undefined, nativeToolChanges ? convertToolDefinitions : undefined);
|
|
909
1099
|
const activeEffort = options?.effort ?? "high";
|
|
910
|
-
const betaFeatures = getBetaFeatures(model, context,
|
|
1100
|
+
const betaFeatures = getBetaFeatures(model, context, options, {
|
|
1101
|
+
isOAuthToken,
|
|
1102
|
+
nativeToolChanges,
|
|
1103
|
+
messageThreads: flags.messageThreads,
|
|
1104
|
+
});
|
|
911
1105
|
const params = {
|
|
912
1106
|
model: model.id,
|
|
913
1107
|
messages: effortMessages ? insertThinkingLevelMessages(converted, activeEffort) : converted.messages,
|
|
@@ -1295,16 +1489,19 @@ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supp
|
|
|
1295
1489
|
if (!tools)
|
|
1296
1490
|
return [];
|
|
1297
1491
|
return tools.map((tool, index) => {
|
|
1298
|
-
const
|
|
1299
|
-
const strict = disablePreferredStrictTools && config && config.type === "json_schema" && config.strict === "prefer"
|
|
1492
|
+
const strict = disablePreferredStrictTools && prefersJsonSchemaStrictSampling(tool)
|
|
1300
1493
|
? undefined
|
|
1301
1494
|
: resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
|
|
1302
1495
|
const parameters = getJsonSchemaToolParameters(tool, strict);
|
|
1303
1496
|
const schema = parameters;
|
|
1497
|
+
// Keep shared definitions so property $refs still resolve. Other root keywords stay out:
|
|
1498
|
+
// Anthropic rejects root anyOf/oneOf/allOf with a 400.
|
|
1304
1499
|
const legacyInputSchema = {
|
|
1305
1500
|
type: "object",
|
|
1306
1501
|
properties: schema.properties ?? {},
|
|
1307
1502
|
required: schema.required ?? [],
|
|
1503
|
+
...(schema.$defs === undefined ? {} : { $defs: schema.$defs }),
|
|
1504
|
+
...(schema.definitions === undefined ? {} : { definitions: schema.definitions }),
|
|
1308
1505
|
};
|
|
1309
1506
|
const inputSchema = strict === true
|
|
1310
1507
|
? {
|