@plumbus/core 0.8.0-beta.1 → 0.8.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +187 -0
- package/README.md +8 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/ai/ai/cost-tracker.d.ts +22 -2
- package/dist/ai/ai/model-pricing.d.ts +8 -2
- package/dist/ai/ai/openai-strict-schema.d.ts +84 -0
- package/dist/ai/ai/provider.d.ts +90 -6
- package/dist/ai/ai-service.d.ts +53 -1
- package/dist/ai/ai-service.d.ts.map +1 -1
- package/dist/ai/ai-service.js +550 -119
- package/dist/ai/ai-service.js.map +1 -1
- package/dist/ai/cost-tracker.d.ts +22 -2
- package/dist/ai/cost-tracker.d.ts.map +1 -1
- package/dist/ai/cost-tracker.js +59 -20
- package/dist/ai/cost-tracker.js.map +1 -1
- package/dist/ai/explainability.d.ts +2 -1
- package/dist/ai/explainability.d.ts.map +1 -1
- package/dist/ai/explainability.js +1 -0
- package/dist/ai/explainability.js.map +1 -1
- package/dist/ai/index.d.ts +2 -2
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +1 -1
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/model-pricing.d.ts +8 -2
- package/dist/ai/model-pricing.d.ts.map +1 -1
- package/dist/ai/model-pricing.js +113 -15
- package/dist/ai/model-pricing.js.map +1 -1
- package/dist/ai/openai-strict-schema.d.ts +85 -0
- package/dist/ai/openai-strict-schema.d.ts.map +1 -0
- package/dist/ai/openai-strict-schema.js +730 -0
- package/dist/ai/openai-strict-schema.js.map +1 -0
- package/dist/ai/prompt-overrides.d.ts +19 -0
- package/dist/ai/prompt-overrides.d.ts.map +1 -0
- package/dist/ai/prompt-overrides.js +60 -0
- package/dist/ai/prompt-overrides.js.map +1 -0
- package/dist/ai/provider-failure.d.ts +9 -0
- package/dist/ai/provider-failure.d.ts.map +1 -0
- package/dist/ai/provider-failure.js +112 -0
- package/dist/ai/provider-failure.js.map +1 -0
- package/dist/ai/provider.d.ts +90 -6
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/provider.js +546 -97
- package/dist/ai/provider.js.map +1 -1
- package/dist/ai/rag/pipeline.d.ts +21 -1
- package/dist/ai/rag/pipeline.d.ts.map +1 -1
- package/dist/ai/rag/pipeline.js +17 -1
- package/dist/ai/rag/pipeline.js.map +1 -1
- package/dist/ai/tool-loop.d.ts +8 -1
- package/dist/ai/tool-loop.d.ts.map +1 -1
- package/dist/ai/tool-loop.js +7 -0
- package/dist/ai/tool-loop.js.map +1 -1
- package/dist/ai/types/config.d.ts +13 -0
- package/dist/ai/types/fields.d.ts +6 -0
- package/dist/ai/types/prompt.d.ts +9 -0
- package/dist/ai/usage-validation.d.ts.map +1 -1
- package/dist/ai/usage-validation.js +6 -1
- package/dist/ai/usage-validation.js.map +1 -1
- package/dist/ai/validation.d.ts +7 -3
- package/dist/ai/validation.d.ts.map +1 -1
- package/dist/ai/validation.js +38 -25
- package/dist/ai/validation.js.map +1 -1
- package/dist/ai/zod-to-provider-schema.d.ts.map +1 -1
- package/dist/ai/zod-to-provider-schema.js +246 -16
- package/dist/ai/zod-to-provider-schema.js.map +1 -1
- package/dist/api/route-generator.d.ts +2 -12
- package/dist/api/route-generator.d.ts.map +1 -1
- package/dist/api/route-generator.js +33 -41
- package/dist/api/route-generator.js.map +1 -1
- package/dist/approvals/index.d.ts +1 -1
- package/dist/approvals/index.d.ts.map +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/approvals/memory-store.d.ts.map +1 -1
- package/dist/approvals/memory-store.js +22 -0
- package/dist/approvals/memory-store.js.map +1 -1
- package/dist/approvals/schema.d.ts +34 -0
- package/dist/approvals/schema.d.ts.map +1 -1
- package/dist/approvals/schema.js +2 -0
- package/dist/approvals/schema.js.map +1 -1
- package/dist/approvals/service.d.ts.map +1 -1
- package/dist/approvals/service.js +36 -0
- package/dist/approvals/service.js.map +1 -1
- package/dist/approvals/sql-store.d.ts.map +1 -1
- package/dist/approvals/sql-store.js +42 -1
- package/dist/approvals/sql-store.js.map +1 -1
- package/dist/approvals/types.d.ts +11 -0
- package/dist/approvals/types.d.ts.map +1 -1
- package/dist/approvals/types.js +1 -0
- package/dist/approvals/types.js.map +1 -1
- package/dist/audit/service.d.ts.map +1 -1
- package/dist/audit/service.js +16 -1
- package/dist/audit/service.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +2 -1
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/commands/browser-extension.d.ts +16 -0
- package/dist/cli/commands/browser-extension.d.ts.map +1 -1
- package/dist/cli/commands/browser-extension.js +16 -5
- package/dist/cli/commands/browser-extension.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +7 -1
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/e2e.d.ts +12 -0
- package/dist/cli/commands/e2e.d.ts.map +1 -1
- package/dist/cli/commands/e2e.js +11 -0
- package/dist/cli/commands/e2e.js.map +1 -1
- package/dist/cli/commands/generate-openapi-from-zod.d.ts.map +1 -1
- package/dist/cli/commands/generate-openapi-from-zod.js +9 -1
- package/dist/cli/commands/generate-openapi-from-zod.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +63 -3
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +8 -14
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/payments.d.ts +56 -0
- package/dist/cli/commands/payments.d.ts.map +1 -0
- package/dist/cli/commands/payments.js +184 -0
- package/dist/cli/commands/payments.js.map +1 -0
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +2 -7
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/seed.d.ts.map +1 -1
- package/dist/cli/commands/seed.js +2 -7
- package/dist/cli/commands/seed.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +9 -1
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/worker.js +1 -1
- package/dist/cli/commands/worker.js.map +1 -1
- package/dist/cli/discover.d.ts +2 -0
- package/dist/cli/discover.d.ts.map +1 -1
- package/dist/cli/discover.js +39 -5
- package/dist/cli/discover.js.map +1 -1
- package/dist/config/loader.d.ts +4 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +112 -24
- package/dist/config/loader.js.map +1 -1
- package/dist/data/connection.d.ts +23 -0
- package/dist/data/connection.d.ts.map +1 -1
- package/dist/data/connection.js +23 -6
- package/dist/data/connection.js.map +1 -1
- package/dist/data/schema-generator.d.ts.map +1 -1
- package/dist/data/schema-generator.js +2 -2
- package/dist/data/schema-generator.js.map +1 -1
- package/dist/define/definePrompt.d.ts.map +1 -1
- package/dist/define/definePrompt.js +6 -0
- package/dist/define/definePrompt.js.map +1 -1
- package/dist/durable/index.d.ts +2 -2
- package/dist/durable/index.d.ts.map +1 -1
- package/dist/durable/index.js +2 -2
- package/dist/durable/index.js.map +1 -1
- package/dist/durable/postgres-persist.d.ts +47 -2
- package/dist/durable/postgres-persist.d.ts.map +1 -1
- package/dist/durable/postgres-persist.js +152 -6
- package/dist/durable/postgres-persist.js.map +1 -1
- package/dist/durable/spine-claim.d.ts +22 -1
- package/dist/durable/spine-claim.d.ts.map +1 -1
- package/dist/durable/spine-claim.js +54 -3
- package/dist/durable/spine-claim.js.map +1 -1
- package/dist/events/dispatcher.d.ts +20 -0
- package/dist/events/dispatcher.d.ts.map +1 -1
- package/dist/events/dispatcher.js +61 -26
- package/dist/events/dispatcher.js.map +1 -1
- package/dist/events/pipeline-audit.d.ts +14 -0
- package/dist/events/pipeline-audit.d.ts.map +1 -0
- package/dist/events/pipeline-audit.js +29 -0
- package/dist/events/pipeline-audit.js.map +1 -0
- package/dist/events/worker.d.ts +3 -0
- package/dist/events/worker.d.ts.map +1 -1
- package/dist/events/worker.js +14 -14
- package/dist/events/worker.js.map +1 -1
- package/dist/execution/capability-error-hook.d.ts +24 -0
- package/dist/execution/capability-error-hook.d.ts.map +1 -0
- package/dist/execution/capability-error-hook.js +17 -0
- package/dist/execution/capability-error-hook.js.map +1 -0
- package/dist/execution/capability-executor.d.ts +2 -2
- package/dist/execution/capability-executor.d.ts.map +1 -1
- package/dist/execution/capability-executor.js +45 -7
- package/dist/execution/capability-executor.js.map +1 -1
- package/dist/execution/context-factory.d.ts.map +1 -1
- package/dist/execution/context-factory.js +9 -1
- package/dist/execution/context-factory.js.map +1 -1
- package/dist/execution/deferred-flow-service.d.ts.map +1 -1
- package/dist/execution/deferred-flow-service.js +7 -0
- package/dist/execution/deferred-flow-service.js.map +1 -1
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js.map +1 -1
- package/dist/fields/index.d.ts +7 -0
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +10 -1
- package/dist/fields/index.js.map +1 -1
- package/dist/flows/dead-letter.d.ts +18 -1
- package/dist/flows/dead-letter.d.ts.map +1 -1
- package/dist/flows/dead-letter.js +17 -1
- package/dist/flows/dead-letter.js.map +1 -1
- package/dist/flows/engine.d.ts +16 -1
- package/dist/flows/engine.d.ts.map +1 -1
- package/dist/flows/engine.js +218 -29
- package/dist/flows/engine.js.map +1 -1
- package/dist/flows/flow-service.d.ts.map +1 -1
- package/dist/flows/flow-service.js +3 -0
- package/dist/flows/flow-service.js.map +1 -1
- package/dist/flows/step-executor.d.ts +8 -0
- package/dist/flows/step-executor.d.ts.map +1 -1
- package/dist/flows/step-executor.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/bootstrap.d.ts +18 -1
- package/dist/runtime/bootstrap.d.ts.map +1 -1
- package/dist/runtime/bootstrap.js +36 -5
- package/dist/runtime/bootstrap.js.map +1 -1
- package/dist/runtime/load-extensions.d.ts +2 -1
- package/dist/runtime/load-extensions.d.ts.map +1 -1
- package/dist/runtime/load-extensions.js +49 -1
- package/dist/runtime/load-extensions.js.map +1 -1
- package/dist/runtime/register-consumers.d.ts +3 -0
- package/dist/runtime/register-consumers.d.ts.map +1 -1
- package/dist/runtime/register-consumers.js +27 -6
- package/dist/runtime/register-consumers.js.map +1 -1
- package/dist/runtime/start-worker-pool.d.ts.map +1 -1
- package/dist/runtime/start-worker-pool.js +36 -15
- package/dist/runtime/start-worker-pool.js.map +1 -1
- package/dist/server/bootstrap.d.ts +42 -16
- package/dist/server/bootstrap.d.ts.map +1 -1
- package/dist/server/bootstrap.js +91 -21
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/testing/context.d.ts +2 -0
- package/dist/testing/context.d.ts.map +1 -1
- package/dist/testing/context.js +8 -3
- package/dist/testing/context.js.map +1 -1
- package/dist/testing/field-validation.d.ts.map +1 -1
- package/dist/testing/field-validation.js +3 -0
- package/dist/testing/field-validation.js.map +1 -1
- package/dist/types/config.d.ts +13 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/context.d.ts +70 -4
- package/dist/types/context.d.ts.map +1 -1
- package/dist/types/fields.d.ts +6 -0
- package/dist/types/fields.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/prompt.d.ts +9 -0
- package/dist/types/prompt.d.ts.map +1 -1
- package/dist/worker/bootstrap.d.ts +19 -0
- package/dist/worker/bootstrap.d.ts.map +1 -1
- package/dist/worker/bootstrap.js +156 -84
- package/dist/worker/bootstrap.js.map +1 -1
- package/instructions/README.md +4 -1
- package/instructions/ai-classification.md +132 -0
- package/instructions/ai.md +94 -15
- package/instructions/cli.md +19 -1
- package/instructions/entities.md +3 -1
- package/instructions/flows.md +6 -0
- package/instructions/framework.md +13 -3
- package/instructions/peer-dependencies.md +2 -1
- package/instructions/prompts.md +2 -1
- package/migrations/durable-tenant/0002_approval_cancellation.sql +3 -0
- package/migrations/durable-tenant/meta/_journal.json +7 -0
- package/package.json +6 -5
- package/dist/ai/__typecheck-fixtures__/legacy-cost-api.d.ts +0 -5
- package/dist/ai/__typecheck-fixtures__/legacy-cost-api.d.ts.map +0 -1
- package/dist/ai/__typecheck-fixtures__/legacy-cost-api.js +0 -15
- package/dist/ai/__typecheck-fixtures__/legacy-cost-api.js.map +0 -1
package/dist/ai/provider.js
CHANGED
|
@@ -2,13 +2,74 @@
|
|
|
2
2
|
// Abstract interface for AI provider adapters (OpenAI, Anthropic, etc.)
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { allKnownModels, estimateModelCost as calculateModelCost, } from './model-pricing.js';
|
|
5
|
+
import { createOpenAIStrictOutputStreamUnwrapper, toOpenAIStrictOutputSchema, unwrapOpenAIStrictOutput, } from './openai-strict-schema.js';
|
|
5
6
|
import { AIIncompleteOutputError, AIInvalidRequestError, AIRefusalError } from './refusal.js';
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a `cache` option into per-section flags.
|
|
9
|
+
* `true` → `{ system: true, tools: true, messages: false }`.
|
|
10
|
+
* `false` / `undefined` / no section enabled → `undefined` (no provider marks).
|
|
11
|
+
*/
|
|
12
|
+
function resolvePromptCache(cache) {
|
|
13
|
+
if (cache === undefined || cache === false)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (cache === true)
|
|
16
|
+
return { system: true, tools: true, messages: false };
|
|
17
|
+
const resolved = {
|
|
18
|
+
system: cache.system === true,
|
|
19
|
+
tools: cache.tools === true,
|
|
20
|
+
messages: cache.messages === true,
|
|
21
|
+
};
|
|
22
|
+
if (!resolved.system && !resolved.tools && !resolved.messages)
|
|
23
|
+
return undefined;
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
26
|
+
const ANTHROPIC_CACHE_CONTROL = Object.freeze({ type: 'ephemeral' });
|
|
27
|
+
/** Attach Anthropic `cache_control` to the last content block of a message. */
|
|
28
|
+
function withAnthropicMessageCacheControl(message) {
|
|
29
|
+
const content = message.content;
|
|
30
|
+
if (typeof content === 'string') {
|
|
31
|
+
return {
|
|
32
|
+
...message,
|
|
33
|
+
content: [{ type: 'text', text: content, cache_control: { ...ANTHROPIC_CACHE_CONTROL } }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
37
|
+
return message;
|
|
38
|
+
const blocks = content.map((block, index) => {
|
|
39
|
+
if (index !== content.length - 1 || block == null || typeof block !== 'object') {
|
|
40
|
+
return block;
|
|
41
|
+
}
|
|
42
|
+
return { ...block, cache_control: { ...ANTHROPIC_CACHE_CONTROL } };
|
|
43
|
+
});
|
|
44
|
+
return { ...message, content: blocks };
|
|
45
|
+
}
|
|
46
|
+
/** Apply Anthropic cache marks to a Messages API request body in place. */
|
|
47
|
+
function applyAnthropicPromptCache(body, cache) {
|
|
48
|
+
if (cache.system && typeof body.system === 'string' && body.system.length > 0) {
|
|
49
|
+
body.system = [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: body.system,
|
|
53
|
+
cache_control: { ...ANTHROPIC_CACHE_CONTROL },
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
if (cache.tools && Array.isArray(body.tools) && body.tools.length > 0) {
|
|
58
|
+
const tools = body.tools;
|
|
59
|
+
const lastIndex = tools.length - 1;
|
|
60
|
+
const last = tools[lastIndex];
|
|
61
|
+
if (last) {
|
|
62
|
+
tools[lastIndex] = { ...last, cache_control: { ...ANTHROPIC_CACHE_CONTROL } };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (cache.messages && Array.isArray(body.messages) && body.messages.length > 0) {
|
|
66
|
+
const messages = body.messages;
|
|
67
|
+
const lastIndex = messages.length - 1;
|
|
68
|
+
const last = messages[lastIndex];
|
|
69
|
+
if (last) {
|
|
70
|
+
messages[lastIndex] = withAnthropicMessageCacheControl(last);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
12
73
|
}
|
|
13
74
|
/**
|
|
14
75
|
* Shared logic used by adapter `listModels` implementations: join a list of
|
|
@@ -20,6 +81,11 @@ function knownReasoningEfforts(provider, model) {
|
|
|
20
81
|
if (provider !== 'openai')
|
|
21
82
|
return null;
|
|
22
83
|
const id = model.replace(/-\d{4}-\d{2}-\d{2}$/, '');
|
|
84
|
+
// https://developers.openai.com/api/docs/guides/latest-model and each model's page.
|
|
85
|
+
if (['gpt-6-sol', 'gpt-6-luna'].includes(id))
|
|
86
|
+
return ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
87
|
+
if (['gpt-6-astra', 'gpt-6.1-sol'].includes(id))
|
|
88
|
+
return ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
23
89
|
if (['gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'].includes(id))
|
|
24
90
|
return ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
25
91
|
if (['gpt-5.2', 'gpt-5.4', 'gpt-5.5'].includes(id))
|
|
@@ -81,15 +147,28 @@ export class ProviderAPIError extends Error {
|
|
|
81
147
|
statusCode;
|
|
82
148
|
retryable;
|
|
83
149
|
attempts;
|
|
150
|
+
/**
|
|
151
|
+
* Set when the request's own timeout ended it (`ProviderRequest.timeout`, a prompt's
|
|
152
|
+
* `model.timeoutMs`, or the adapter's `requestTimeout`): the limit that was reached, in
|
|
153
|
+
* milliseconds. Such a failure is not retryable — the provider may have billed the work, and
|
|
154
|
+
* asking again with the same limit usually ends the same way.
|
|
155
|
+
*/
|
|
156
|
+
timeoutMs;
|
|
84
157
|
constructor(args) {
|
|
85
|
-
super(args.message);
|
|
158
|
+
super(args.message, args.cause === undefined ? undefined : { cause: args.cause });
|
|
86
159
|
this.name = 'ProviderAPIError';
|
|
87
160
|
this.providerName = args.providerName;
|
|
88
161
|
this.statusCode = args.statusCode;
|
|
89
162
|
this.retryable = args.retryable;
|
|
90
163
|
this.attempts = args.attempts;
|
|
164
|
+
if (args.timeoutMs !== undefined)
|
|
165
|
+
this.timeoutMs = args.timeoutMs;
|
|
91
166
|
}
|
|
92
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* True for a provider HTTP failure from any adapter: a `ProviderAPIError` instance, or an error
|
|
170
|
+
* of the same shape built by an adapter package with its own copy of core (`@plumbus/ai-bedrock`).
|
|
171
|
+
*/
|
|
93
172
|
export function isProviderAPIError(value) {
|
|
94
173
|
if (value instanceof ProviderAPIError)
|
|
95
174
|
return true;
|
|
@@ -102,6 +181,14 @@ export function isProviderAPIError(value) {
|
|
|
102
181
|
typeof candidate.retryable === 'boolean' &&
|
|
103
182
|
typeof candidate.attempts === 'number');
|
|
104
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* True when the request's own timeout ended it (`timeoutMs` on the call, a prompt override, the
|
|
186
|
+
* prompt's `model.timeoutMs`, or the adapter's `requestTimeout`): a `ProviderAPIError` carrying
|
|
187
|
+
* the limit reached as `timeoutMs`. Not retryable; raise the timeout rather than asking again.
|
|
188
|
+
*/
|
|
189
|
+
export function isProviderTimeoutError(value) {
|
|
190
|
+
return isProviderAPIError(value) && typeof value.timeoutMs === 'number';
|
|
191
|
+
}
|
|
105
192
|
const retryableStatusCodes = new Set([408, 429, 500, 502, 503, 504]);
|
|
106
193
|
const maxProviderAttempts = 3;
|
|
107
194
|
const baseRetryDelayMs = 750;
|
|
@@ -128,6 +215,13 @@ function computeRetryDelayMs(attempt, retryAfterHeader) {
|
|
|
128
215
|
}
|
|
129
216
|
return Math.min(baseRetryDelayMs * 2 ** (attempt - 1), maxRetryDelayMs);
|
|
130
217
|
}
|
|
218
|
+
// Every OpenAI structured-output path is strict, and strict mode refuses optional properties:
|
|
219
|
+
// each one goes out as required-but-nullable. The AI service drops the nulls again before the
|
|
220
|
+
// prompt's Zod schema validates the answer (`restoreOmittedOptionalProperties`). String formats
|
|
221
|
+
// strict mode does not support (`uri`) are left out of the sent schema, and `allOf` is merged
|
|
222
|
+
// into one schema; Zod still checks the original. A root that is not an object (an array, a
|
|
223
|
+
// union) goes inside a `{ "value": ... }` envelope that `unwrapOpenAIStructuredContent` (or, on a
|
|
224
|
+
// stream, `unwrapOpenAIStructuredStream`) takes off the answer again.
|
|
131
225
|
function buildOpenAIResponseFormat(request) {
|
|
132
226
|
if (request.responseFormat !== 'json')
|
|
133
227
|
return undefined;
|
|
@@ -138,27 +232,63 @@ function buildOpenAIResponseFormat(request) {
|
|
|
138
232
|
json_schema: {
|
|
139
233
|
name: 'response',
|
|
140
234
|
strict: true,
|
|
141
|
-
schema: request.responseSchema,
|
|
235
|
+
schema: toOpenAIStrictOutputSchema(request.responseSchema).schema,
|
|
142
236
|
},
|
|
143
237
|
};
|
|
144
238
|
}
|
|
239
|
+
/** Whether the request's strict structured output went out inside the root envelope. */
|
|
240
|
+
function hasOpenAIStructuredEnvelope(request) {
|
|
241
|
+
return (request.responseFormat === 'json' &&
|
|
242
|
+
!!request.responseSchema &&
|
|
243
|
+
toOpenAIStrictOutputSchema(request.responseSchema).enveloped);
|
|
244
|
+
}
|
|
245
|
+
/** The answer as the caller's schema describes it: out of the root envelope if it was sent in one. */
|
|
246
|
+
function unwrapOpenAIStructuredContent(request, content) {
|
|
247
|
+
return hasOpenAIStructuredEnvelope(request) ? unwrapOpenAIStrictOutput(content) : content;
|
|
248
|
+
}
|
|
249
|
+
/** A Chat Completions stream with the root envelope taken out of its text deltas. */
|
|
250
|
+
async function* unwrapOpenAIStructuredStream(events) {
|
|
251
|
+
const unwrapper = createOpenAIStrictOutputStreamUnwrapper();
|
|
252
|
+
let ended = false;
|
|
253
|
+
for await (const event of events) {
|
|
254
|
+
if (event.type === 'content_delta' && typeof event.delta === 'string') {
|
|
255
|
+
const delta = unwrapper.push(event.delta);
|
|
256
|
+
if (delta)
|
|
257
|
+
yield { ...event, delta };
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (event.type === 'done' && !ended) {
|
|
261
|
+
ended = true;
|
|
262
|
+
const rest = unwrapper.end();
|
|
263
|
+
if (rest)
|
|
264
|
+
yield { type: 'content_delta', delta: rest };
|
|
265
|
+
}
|
|
266
|
+
yield event;
|
|
267
|
+
}
|
|
268
|
+
if (!ended) {
|
|
269
|
+
const rest = unwrapper.end();
|
|
270
|
+
if (rest)
|
|
271
|
+
yield { type: 'content_delta', delta: rest };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
145
274
|
const OPENAI_STRUCTURED_OUTPUT_TOOL_NAME = 'return_structured_response';
|
|
146
275
|
function shouldUseOpenAIStructuredTool(args) {
|
|
147
276
|
return (args.request.structuredOutputTransport === 'tool' &&
|
|
148
277
|
args.request.responseFormat === 'json' &&
|
|
149
278
|
!!args.request.responseSchema);
|
|
150
279
|
}
|
|
151
|
-
function
|
|
280
|
+
/** The structured-output function; Chat Completions nests it under `function`, Responses does not. */
|
|
281
|
+
function openAIStructuredOutputFunction(schema) {
|
|
152
282
|
return {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
strict: true,
|
|
158
|
-
parameters: schema,
|
|
159
|
-
},
|
|
283
|
+
name: OPENAI_STRUCTURED_OUTPUT_TOOL_NAME,
|
|
284
|
+
description: 'Return the complete structured response for this request.',
|
|
285
|
+
strict: true,
|
|
286
|
+
parameters: toOpenAIStrictOutputSchema(schema).schema,
|
|
160
287
|
};
|
|
161
288
|
}
|
|
289
|
+
function buildOpenAIStructuredOutputTool(schema) {
|
|
290
|
+
return { type: 'function', function: openAIStructuredOutputFunction(schema) };
|
|
291
|
+
}
|
|
162
292
|
function extractOpenAIMessageContent(message) {
|
|
163
293
|
const structuredToolCall = message.tool_calls?.find((toolCall) => toolCall.function?.name === OPENAI_STRUCTURED_OUTPUT_TOOL_NAME);
|
|
164
294
|
const toolArguments = structuredToolCall?.function?.arguments;
|
|
@@ -183,11 +313,16 @@ function buildEmptyOpenAIStructuredContentError(args) {
|
|
|
183
313
|
}
|
|
184
314
|
function openAIUsageToTokenUsage(usage) {
|
|
185
315
|
const reasoning = usage.completion_tokens_details?.reasoning_tokens;
|
|
316
|
+
const cacheWrites = usage.prompt_tokens_details?.cache_write_tokens;
|
|
186
317
|
return {
|
|
187
318
|
inputTokens: usage.prompt_tokens,
|
|
188
319
|
outputTokens: usage.completion_tokens,
|
|
189
320
|
totalTokens: usage.total_tokens,
|
|
190
321
|
cachedInputTokens: usage.prompt_tokens_details?.cached_tokens ?? 0,
|
|
322
|
+
// Prompt tokens written to the cache, counted inside `prompt_tokens` like cache reads. GPT-5.6
|
|
323
|
+
// and later bill them at 1.25× input (`estimateModelCost`); unread they were priced as
|
|
324
|
+
// ordinary input.
|
|
325
|
+
...(typeof cacheWrites === 'number' ? { cacheWriteTokens: cacheWrites } : {}),
|
|
191
326
|
// Reasoning models count their thinking inside `completion_tokens`. Without this split a
|
|
192
327
|
// caller that hit `max_completion_tokens` cannot tell a long answer from a long deliberation,
|
|
193
328
|
// and can only guess at which knob to turn.
|
|
@@ -196,19 +331,26 @@ function openAIUsageToTokenUsage(usage) {
|
|
|
196
331
|
}
|
|
197
332
|
function shouldUseOpenAIMaxCompletionTokens(model) {
|
|
198
333
|
const normalizedModel = model.trim().toLowerCase();
|
|
199
|
-
return /^(?:o\d|o\d-|o\d\.)/.test(normalizedModel) ||
|
|
334
|
+
return (/^(?:o\d|o\d-|o\d\.)/.test(normalizedModel) ||
|
|
335
|
+
normalizedModel.startsWith('gpt-5') ||
|
|
336
|
+
normalizedModel.startsWith('gpt-6'));
|
|
200
337
|
}
|
|
201
338
|
/**
|
|
202
|
-
* `gpt-5.5+` only accept the API default temperature (1).
|
|
203
|
-
*
|
|
339
|
+
* `gpt-5.5+` only accept the API default temperature (1). GPT-6 accepts
|
|
340
|
+
* temperature only when reasoning is explicitly disabled. Sending any other
|
|
341
|
+
* value with active/default GPT-6 reasoning returns HTTP 400.
|
|
204
342
|
*
|
|
205
343
|
* Do not broaden this to all `gpt-5*` models — earlier lines such as
|
|
206
344
|
* `gpt-5.4` / `gpt-5.4-mini` still support custom temperature. Token-limit
|
|
207
345
|
* mapping (`max_completion_tokens`) remains a separate, wider check.
|
|
208
346
|
*/
|
|
209
|
-
function shouldOmitOpenAITemperature(model) {
|
|
347
|
+
function shouldOmitOpenAITemperature(model, request) {
|
|
210
348
|
const normalizedModel = model.trim().toLowerCase();
|
|
211
|
-
|
|
349
|
+
if (/^gpt-5\.(?:[5-9]|\d{2,})(?:$|[-.])/.test(normalizedModel))
|
|
350
|
+
return true;
|
|
351
|
+
if (!isGpt6Family(model))
|
|
352
|
+
return false;
|
|
353
|
+
return normalizeProviderRequestReasoning(request)?.mode !== 'disabled';
|
|
212
354
|
}
|
|
213
355
|
function applyOpenAITokenLimit(body, model, maxTokens) {
|
|
214
356
|
if (!maxTokens)
|
|
@@ -219,10 +361,10 @@ function applyOpenAITokenLimit(body, model, maxTokens) {
|
|
|
219
361
|
}
|
|
220
362
|
body.max_tokens = maxTokens;
|
|
221
363
|
}
|
|
222
|
-
function applyOpenAITemperature(body, model,
|
|
223
|
-
if (shouldOmitOpenAITemperature(model))
|
|
364
|
+
function applyOpenAITemperature(body, model, request) {
|
|
365
|
+
if (shouldOmitOpenAITemperature(model, request))
|
|
224
366
|
return;
|
|
225
|
-
body.temperature = temperature ?? 0.7;
|
|
367
|
+
body.temperature = request.temperature ?? 0.7;
|
|
226
368
|
}
|
|
227
369
|
function normalizeProviderRequestReasoning(request) {
|
|
228
370
|
return request.reasoning;
|
|
@@ -233,8 +375,28 @@ function unsupportedReasoning(provider, reasoning) {
|
|
|
233
375
|
message: `AI provider "${provider}" cannot represent reasoning mode "${reasoning.mode}"`,
|
|
234
376
|
});
|
|
235
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* GPT-6 models accept no `minimal` effort, and GPT-6 Astra and GPT-6.1 Sol also
|
|
380
|
+
* reject `none`, so disabled reasoning cannot be represented for them.
|
|
381
|
+
*/
|
|
382
|
+
function assertOpenAIReasoningSupported(model, reasoning) {
|
|
383
|
+
if (!isGpt6Family(model))
|
|
384
|
+
return;
|
|
385
|
+
if (reasoning.mode === 'effort' && reasoning.effort === 'minimal') {
|
|
386
|
+
throw new AIInvalidRequestError({
|
|
387
|
+
reason: 'reasoning_effort_unsupported',
|
|
388
|
+
message: `OpenAI model "${model}" does not support reasoning effort "minimal"; use "low"`,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (reasoning.mode === 'disabled' && isGpt6WithoutNoneEffort(model)) {
|
|
392
|
+
throw new AIInvalidRequestError({
|
|
393
|
+
reason: 'reasoning_disabled_unsupported',
|
|
394
|
+
message: `OpenAI model "${model}" cannot disable reasoning; its lowest effort is "low"`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
236
398
|
/** Translate provider-neutral reasoning onto OpenAI's top-level `reasoning_effort`. */
|
|
237
|
-
function applyOpenAIReasoning(body, request) {
|
|
399
|
+
function applyOpenAIReasoning(body, request, model) {
|
|
238
400
|
const reasoning = normalizeProviderRequestReasoning(request);
|
|
239
401
|
if (!reasoning) {
|
|
240
402
|
if (request.reasoningEffort)
|
|
@@ -243,11 +405,70 @@ function applyOpenAIReasoning(body, request) {
|
|
|
243
405
|
}
|
|
244
406
|
if (reasoning.mode === 'budget')
|
|
245
407
|
unsupportedReasoning('openai', reasoning);
|
|
408
|
+
assertOpenAIReasoningSupported(model, reasoning);
|
|
246
409
|
body.reasoning_effort = reasoning.mode === 'disabled' ? 'none' : reasoning.effort;
|
|
247
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Claude models that return HTTP 400 for any non-default `temperature`, `top_p` or
|
|
413
|
+
* `top_k` on every request, thinking on or off. `disabled` is the `thinking` type sent
|
|
414
|
+
* for `{ mode: 'disabled' }` (Sonnet 5.5 takes `between_tools` instead); a model
|
|
415
|
+
* without it cannot turn thinking off. Opus 5, Sonnet 5.5 and Haiku 5.5 accept that
|
|
416
|
+
* type only at effort `high` or below; a disabled request sends no effort, and their
|
|
417
|
+
* defaults are `high` (`medium` on Haiku 5.5). Of these models only Mythos Preview
|
|
418
|
+
* accepts `thinking: { type: 'enabled', budget_tokens }`.
|
|
419
|
+
* Sources: https://platform.claude.com/docs/en/build-with-claude/thinking#sampling-parameters
|
|
420
|
+
* and https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting
|
|
421
|
+
*/
|
|
422
|
+
const FIXED_SAMPLING_CLAUDE_MODELS = {
|
|
423
|
+
'fable-5-1': {},
|
|
424
|
+
'mythos-5-1': {},
|
|
425
|
+
'fable-5': {},
|
|
426
|
+
'mythos-5': {},
|
|
427
|
+
'mythos-preview': { budget: true },
|
|
428
|
+
'opus-5-5': {},
|
|
429
|
+
'opus-5': { disabled: 'disabled' },
|
|
430
|
+
'opus-4-8': { disabled: 'disabled' },
|
|
431
|
+
'opus-4-7': { disabled: 'disabled' },
|
|
432
|
+
'sonnet-5-5': { disabled: 'between_tools' },
|
|
433
|
+
'sonnet-5': { disabled: 'disabled' },
|
|
434
|
+
'haiku-5-5': { disabled: 'disabled' },
|
|
435
|
+
};
|
|
436
|
+
/** Looks up a fixed-sampling Claude model, including dated and platform-prefixed ids. */
|
|
437
|
+
function fixedSamplingClaudeModel(model) {
|
|
438
|
+
if (typeof model !== 'string')
|
|
439
|
+
return undefined;
|
|
440
|
+
const normalizedModel = model.trim().toLowerCase();
|
|
441
|
+
const id = /(?:^|[./])claude-([a-z]+-(?:\d+(?:-\d{1,2})?|preview))(?=$|[-@:])/.exec(normalizedModel)?.[1];
|
|
442
|
+
return id && Object.hasOwn(FIXED_SAMPLING_CLAUDE_MODELS, id)
|
|
443
|
+
? FIXED_SAMPLING_CLAUDE_MODELS[id]
|
|
444
|
+
: undefined;
|
|
445
|
+
}
|
|
248
446
|
/** Translate provider-neutral reasoning onto Anthropic Messages API fields. */
|
|
249
447
|
function applyAnthropicReasoning(body, request) {
|
|
250
448
|
const reasoning = normalizeProviderRequestReasoning(request);
|
|
449
|
+
const fixedSampling = fixedSamplingClaudeModel(body.model);
|
|
450
|
+
// These models accept only the API default, so any temperature (Plumbus's 0.7
|
|
451
|
+
// default or a prompt's own) is left out, as for OpenAI gpt-5.5+.
|
|
452
|
+
const temperature = fixedSampling ? undefined : request.temperature;
|
|
453
|
+
if (fixedSampling) {
|
|
454
|
+
delete body.temperature;
|
|
455
|
+
if (reasoning?.mode === 'budget' && !fixedSampling.budget) {
|
|
456
|
+
throw new AIInvalidRequestError({
|
|
457
|
+
reason: 'reasoning_budget_unsupported',
|
|
458
|
+
message: `Anthropic model "${body.model}" does not accept a thinking budget; use an effort`,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
if (reasoning?.mode === 'disabled') {
|
|
462
|
+
if (!fixedSampling.disabled) {
|
|
463
|
+
throw new AIInvalidRequestError({
|
|
464
|
+
reason: 'reasoning_disabled_unsupported',
|
|
465
|
+
message: `Anthropic model "${body.model}" cannot turn thinking off; use effort "low"`,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
body.thinking = { type: fixedSampling.disabled };
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
251
472
|
if (!reasoning)
|
|
252
473
|
return;
|
|
253
474
|
if (reasoning.mode === 'disabled') {
|
|
@@ -267,13 +488,13 @@ function applyAnthropicReasoning(body, request) {
|
|
|
267
488
|
message: 'Anthropic reasoning budget must be smaller than maxTokens',
|
|
268
489
|
});
|
|
269
490
|
}
|
|
270
|
-
if (
|
|
491
|
+
if (temperature != null && temperature !== 1) {
|
|
271
492
|
throw new AIInvalidRequestError({
|
|
272
493
|
reason: 'reasoning_temperature_conflict',
|
|
273
494
|
message: 'Anthropic thinking requires temperature 1 or an omitted temperature',
|
|
274
495
|
});
|
|
275
496
|
}
|
|
276
|
-
if (
|
|
497
|
+
if (temperature == null)
|
|
277
498
|
delete body.temperature;
|
|
278
499
|
body.thinking = {
|
|
279
500
|
type: 'enabled',
|
|
@@ -285,13 +506,13 @@ function applyAnthropicReasoning(body, request) {
|
|
|
285
506
|
if (reasoning.effort === 'minimal') {
|
|
286
507
|
unsupportedReasoning('anthropic', reasoning);
|
|
287
508
|
}
|
|
288
|
-
if (
|
|
509
|
+
if (temperature != null && temperature !== 1) {
|
|
289
510
|
throw new AIInvalidRequestError({
|
|
290
511
|
reason: 'reasoning_temperature_conflict',
|
|
291
512
|
message: 'Anthropic adaptive thinking requires temperature 1 or an omitted temperature',
|
|
292
513
|
});
|
|
293
514
|
}
|
|
294
|
-
if (
|
|
515
|
+
if (temperature == null)
|
|
295
516
|
delete body.temperature;
|
|
296
517
|
body.thinking = { type: 'adaptive', display: 'omitted' };
|
|
297
518
|
const outputConfig = body.output_config && typeof body.output_config === 'object'
|
|
@@ -321,30 +542,157 @@ function buildProviderErrorMessage(label, statusCode, body, attempts) {
|
|
|
321
542
|
const detail = body || 'Request failed';
|
|
322
543
|
return `${label} error (${statusCode})${suffix}: ${detail}`;
|
|
323
544
|
}
|
|
545
|
+
// ── Request deadlines ──
|
|
546
|
+
/** The longest delay Node's timers keep: `AbortSignal.timeout` fires at once above it. */
|
|
547
|
+
const MAX_REQUEST_TIMEOUT_MS = 2_147_483_647;
|
|
548
|
+
/**
|
|
549
|
+
* Where Node's fetch (undici, bundled or installed) keeps the process's dispatcher: undici 7.27+
|
|
550
|
+
* under `.2` (and the same agent under `.1`), earlier versions under `.1` only.
|
|
551
|
+
*/
|
|
552
|
+
const UNDICI_GLOBAL_DISPATCHERS = [
|
|
553
|
+
Symbol.for('undici.globalDispatcher.2'),
|
|
554
|
+
Symbol.for('undici.globalDispatcher.1'),
|
|
555
|
+
];
|
|
556
|
+
function globalUndiciDispatcher() {
|
|
557
|
+
for (const key of UNDICI_GLOBAL_DISPATCHERS) {
|
|
558
|
+
const dispatcher = globalThis[key];
|
|
559
|
+
if (typeof dispatcher?.dispatch === 'function') {
|
|
560
|
+
return dispatcher;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* How far above the request's timeout undici's own limits are set. Undici runs them on coarse
|
|
567
|
+
* timers (a 499 ms tick) that can fire up to half a second before their delay, so limits equal
|
|
568
|
+
* to the timeout beat the request's own timer on a reused connection and the call failed as a
|
|
569
|
+
* bare `fetch failed` instead of the timeout. One second keeps undici's limits strictly behind.
|
|
570
|
+
*/
|
|
571
|
+
const TRANSPORT_LIMIT_MARGIN_MS = 1_000;
|
|
572
|
+
/**
|
|
573
|
+
* Node's fetch gives up by itself after 300 s without response headers, or without the next
|
|
574
|
+
* body chunk (undici's `headersTimeout` / `bodyTimeout`). A non-streamed answer that took longer
|
|
575
|
+
* to generate failed with `fetch failed` (`UND_ERR_HEADERS_TIMEOUT`) whatever the request's
|
|
576
|
+
* timeout allowed. This dispatcher passes each request to the process's dispatcher — a proxy or
|
|
577
|
+
* agent the application installed included — with both limits set just above the request's
|
|
578
|
+
* timeout, so that timeout alone decides. Fetch implementations other than undici ignore
|
|
579
|
+
* `dispatcher`.
|
|
580
|
+
*/
|
|
581
|
+
function deadlineDispatcher(timeoutMs) {
|
|
582
|
+
const transportLimitMs = Math.min(timeoutMs + TRANSPORT_LIMIT_MARGIN_MS, MAX_REQUEST_TIMEOUT_MS);
|
|
583
|
+
return {
|
|
584
|
+
get isMockActive() {
|
|
585
|
+
return globalUndiciDispatcher()?.isMockActive;
|
|
586
|
+
},
|
|
587
|
+
dispatch(options, handler) {
|
|
588
|
+
const dispatcher = globalUndiciDispatcher();
|
|
589
|
+
if (!dispatcher)
|
|
590
|
+
throw new Error('Node fetch has no global dispatcher');
|
|
591
|
+
return dispatcher.dispatch({ ...options, headersTimeout: transportLimitMs, bodyTimeout: transportLimitMs }, handler);
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/** `error` and the errors it wraps (`cause`), outermost first. */
|
|
596
|
+
function errorChain(error) {
|
|
597
|
+
const chain = [];
|
|
598
|
+
let current = error;
|
|
599
|
+
for (let depth = 0; depth < 5 && typeof current === 'object' && current !== null; depth += 1) {
|
|
600
|
+
chain.push(current);
|
|
601
|
+
current = current.cause;
|
|
602
|
+
}
|
|
603
|
+
return chain;
|
|
604
|
+
}
|
|
605
|
+
/** Throws `AIInvalidRequestError` (before any I/O) unless `timeoutMs` is a usable request timeout. */
|
|
606
|
+
export function assertRequestTimeout(timeoutMs) {
|
|
607
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_REQUEST_TIMEOUT_MS) {
|
|
608
|
+
throw new AIInvalidRequestError({
|
|
609
|
+
reason: 'timeout_invalid',
|
|
610
|
+
message: `AI request timeout must be a whole number of milliseconds from 1 to ${MAX_REQUEST_TIMEOUT_MS}; got ${String(timeoutMs)}`,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function createRequestDeadline(args) {
|
|
615
|
+
const timeoutMs = args.timeout ?? args.defaultTimeout;
|
|
616
|
+
assertRequestTimeout(timeoutMs);
|
|
617
|
+
let attempts = 0;
|
|
618
|
+
let timer;
|
|
619
|
+
return {
|
|
620
|
+
providerName: args.providerName,
|
|
621
|
+
errorLabel: args.errorLabel,
|
|
622
|
+
timeoutMs,
|
|
623
|
+
attempt() {
|
|
624
|
+
attempts += 1;
|
|
625
|
+
timer = AbortSignal.timeout(timeoutMs);
|
|
626
|
+
return {
|
|
627
|
+
signal: args.signal ? AbortSignal.any([args.signal, timer]) : timer,
|
|
628
|
+
dispatcher: deadlineDispatcher(timeoutMs),
|
|
629
|
+
};
|
|
630
|
+
},
|
|
631
|
+
classify(error) {
|
|
632
|
+
if (!timer?.aborted || isProviderAPIError(error))
|
|
633
|
+
return error;
|
|
634
|
+
const chain = errorChain(error);
|
|
635
|
+
const aborted = chain.some((link) => {
|
|
636
|
+
const name = link.name;
|
|
637
|
+
return link === timer?.reason || name === 'TimeoutError' || name === 'AbortError';
|
|
638
|
+
});
|
|
639
|
+
if (!aborted)
|
|
640
|
+
return error;
|
|
641
|
+
// The caller's own cancellation is rethrown as raised, unless the timeout fired first.
|
|
642
|
+
if (args.signal?.aborted && !chain.includes(timer.reason))
|
|
643
|
+
return error;
|
|
644
|
+
return new ProviderAPIError({
|
|
645
|
+
providerName: args.providerName,
|
|
646
|
+
message: `${args.errorLabel} request timed out after ${timeoutMs} ms`,
|
|
647
|
+
retryable: false,
|
|
648
|
+
attempts,
|
|
649
|
+
timeoutMs,
|
|
650
|
+
cause: error,
|
|
651
|
+
});
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
/** Reads a JSON answer; a body the request's timeout cut off fails as that timeout. */
|
|
656
|
+
async function readResponseJson(response, deadline) {
|
|
657
|
+
try {
|
|
658
|
+
return await response.json();
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
throw deadline.classify(error);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
324
664
|
async function executeRequestWithRetry(args) {
|
|
665
|
+
const { deadline } = args;
|
|
325
666
|
for (let attempt = 1; attempt <= maxProviderAttempts; attempt++) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
667
|
+
let response;
|
|
668
|
+
let body;
|
|
669
|
+
try {
|
|
670
|
+
response = await args.send(deadline.attempt());
|
|
671
|
+
if (response.ok) {
|
|
672
|
+
return response;
|
|
673
|
+
}
|
|
674
|
+
body = await response.text();
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
throw deadline.classify(error);
|
|
329
678
|
}
|
|
330
|
-
const body = await response.text();
|
|
331
679
|
const retryable = isRetryableStatusCode(response.status);
|
|
332
680
|
if (!retryable || attempt === maxProviderAttempts) {
|
|
333
681
|
throw new ProviderAPIError({
|
|
334
|
-
providerName:
|
|
682
|
+
providerName: deadline.providerName,
|
|
335
683
|
statusCode: response.status,
|
|
336
684
|
retryable,
|
|
337
685
|
attempts: attempt,
|
|
338
|
-
message: buildProviderErrorMessage(
|
|
686
|
+
message: buildProviderErrorMessage(deadline.errorLabel, response.status, body, attempt),
|
|
339
687
|
});
|
|
340
688
|
}
|
|
341
689
|
await waitForRetry(computeRetryDelayMs(attempt, response.headers.get('retry-after')));
|
|
342
690
|
}
|
|
343
691
|
throw new ProviderAPIError({
|
|
344
|
-
providerName:
|
|
692
|
+
providerName: deadline.providerName,
|
|
345
693
|
retryable: false,
|
|
346
694
|
attempts: maxProviderAttempts,
|
|
347
|
-
message: `${
|
|
695
|
+
message: `${deadline.errorLabel} request failed`,
|
|
348
696
|
});
|
|
349
697
|
}
|
|
350
698
|
// ── Provider tool-calling helpers ──
|
|
@@ -480,23 +828,39 @@ function hasOpenAIResponsesContinuation(request) {
|
|
|
480
828
|
function isGpt56Family(model) {
|
|
481
829
|
return /^gpt-5\.6(?:$|[-.])/.test(model.trim().toLowerCase());
|
|
482
830
|
}
|
|
831
|
+
function isGpt6Family(model) {
|
|
832
|
+
return /^gpt-6(?:$|[-.])/.test(model.trim().toLowerCase());
|
|
833
|
+
}
|
|
834
|
+
/** GPT-6 Astra and GPT-6.1 Sol: lowest reasoning effort is `low`, not `none`. */
|
|
835
|
+
function isGpt6WithoutNoneEffort(model) {
|
|
836
|
+
return /^gpt-6(?:-astra|\.1-sol)(?:$|-)/.test(model.trim().toLowerCase());
|
|
837
|
+
}
|
|
483
838
|
/**
|
|
484
839
|
* Chat Completions cannot combine function tools with active reasoning on
|
|
485
|
-
* GPT-5.6
|
|
486
|
-
*
|
|
487
|
-
*
|
|
840
|
+
* GPT-5.6 or GPT-6 ("Function tools with reasoning_effort are not supported for
|
|
841
|
+
* gpt-6-luna in /v1/chat/completions. To use function tools, use /v1/responses or
|
|
842
|
+
* set reasoning_effort to 'none'."). Keep every compatible request on the existing
|
|
843
|
+
* transport. Caller tools use Responses for explicit reasoning effort, these families'
|
|
844
|
+
* default reasoning, and continuation rounds that already carry Responses output
|
|
845
|
+
* state. The structured-output tool of `structuredOutputTransport: 'tool'` is one
|
|
846
|
+
* forced call with no continuation, so it moves only where Chat Completions refuses
|
|
847
|
+
* it: these families with reasoning not disabled. On other models (o-series, earlier
|
|
848
|
+
* gpt-5 lines, an OpenAI-compatible `baseUrl` that may have no `/responses`) it stays
|
|
849
|
+
* on Chat Completions, with its reasoning effort, as before.
|
|
488
850
|
*/
|
|
489
851
|
function shouldUseOpenAIResponses(request, model) {
|
|
490
852
|
if (hasOpenAIResponsesContinuation(request))
|
|
491
853
|
return true;
|
|
492
|
-
if (!request.tools || request.tools.length === 0)
|
|
493
|
-
return false;
|
|
494
854
|
const reasoning = normalizeProviderRequestReasoning(request);
|
|
495
855
|
if (reasoning?.mode === 'disabled')
|
|
496
856
|
return false;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
857
|
+
const chatRefusesToolsUnderReasoning = isGpt56Family(model) || isGpt6Family(model);
|
|
858
|
+
if (request.tools && request.tools.length > 0) {
|
|
859
|
+
if (reasoning?.mode === 'effort' || request.reasoningEffort !== undefined)
|
|
860
|
+
return true;
|
|
861
|
+
return reasoning === undefined && chatRefusesToolsUnderReasoning;
|
|
862
|
+
}
|
|
863
|
+
return shouldUseOpenAIStructuredTool({ request }) && chatRefusesToolsUnderReasoning;
|
|
500
864
|
}
|
|
501
865
|
function toOpenAIResponsesInput(request) {
|
|
502
866
|
if (!request.messages || request.messages.length === 0) {
|
|
@@ -544,7 +908,7 @@ function buildOpenAIResponsesToolChoice(toolChoice) {
|
|
|
544
908
|
return toolChoice;
|
|
545
909
|
return { type: 'function', name: toolChoice.function.name };
|
|
546
910
|
}
|
|
547
|
-
function applyOpenAIResponsesReasoning(body, request) {
|
|
911
|
+
function applyOpenAIResponsesReasoning(body, request, model) {
|
|
548
912
|
const reasoning = normalizeProviderRequestReasoning(request);
|
|
549
913
|
if (!reasoning) {
|
|
550
914
|
if (request.reasoningEffort)
|
|
@@ -553,16 +917,24 @@ function applyOpenAIResponsesReasoning(body, request) {
|
|
|
553
917
|
}
|
|
554
918
|
if (reasoning.mode === 'budget')
|
|
555
919
|
unsupportedReasoning('openai', reasoning);
|
|
920
|
+
assertOpenAIReasoningSupported(model, reasoning);
|
|
556
921
|
body.reasoning = { effort: reasoning.mode === 'disabled' ? 'none' : reasoning.effort };
|
|
557
922
|
}
|
|
558
923
|
function openAIResponsesUsageToTokenUsage(usage) {
|
|
559
924
|
const inputTokens = usage?.input_tokens ?? 0;
|
|
560
925
|
const outputTokens = usage?.output_tokens ?? 0;
|
|
926
|
+
const reasoning = usage?.output_tokens_details?.reasoning_tokens;
|
|
927
|
+
const cacheWrites = usage?.input_tokens_details?.cache_write_tokens;
|
|
561
928
|
return {
|
|
562
929
|
inputTokens,
|
|
563
930
|
outputTokens,
|
|
564
931
|
totalTokens: usage?.total_tokens ?? inputTokens + outputTokens,
|
|
565
932
|
cachedInputTokens: usage?.input_tokens_details?.cached_tokens ?? 0,
|
|
933
|
+
// As on Chat Completions: the share of `input_tokens` written to the cache.
|
|
934
|
+
...(typeof cacheWrites === 'number' ? { cacheWriteTokens: cacheWrites } : {}),
|
|
935
|
+
// As on Chat Completions (`openAIUsageToTokenUsage`): the reasoning share of `output_tokens`,
|
|
936
|
+
// which already counts it and is what is priced.
|
|
937
|
+
...(typeof reasoning === 'number' ? { reasoningTokens: reasoning } : {}),
|
|
566
938
|
};
|
|
567
939
|
}
|
|
568
940
|
function extractOpenAIResponsesContent(output) {
|
|
@@ -597,6 +969,11 @@ async function completeOpenAIResponses(args) {
|
|
|
597
969
|
assertNoStructuredOutputToolConflict(request);
|
|
598
970
|
validateCallerTools(request.tools);
|
|
599
971
|
}
|
|
972
|
+
// `structuredOutputTransport: 'tool'` under active reasoning: the same forced strict function
|
|
973
|
+
// as on Chat Completions, in the Responses tool shape (its fields sit on the tool itself).
|
|
974
|
+
const structuredToolSchema = shouldUseOpenAIStructuredTool({ request })
|
|
975
|
+
? request.responseSchema
|
|
976
|
+
: undefined;
|
|
600
977
|
const body = {
|
|
601
978
|
model,
|
|
602
979
|
input: toOpenAIResponsesInput(request),
|
|
@@ -605,16 +982,38 @@ async function completeOpenAIResponses(args) {
|
|
|
605
982
|
};
|
|
606
983
|
if (request.system)
|
|
607
984
|
body.instructions = request.system;
|
|
608
|
-
applyOpenAITemperature(body, model, request
|
|
609
|
-
applyOpenAIResponsesReasoning(body, request);
|
|
985
|
+
applyOpenAITemperature(body, model, request);
|
|
986
|
+
applyOpenAIResponsesReasoning(body, request, model);
|
|
610
987
|
if (request.maxTokens)
|
|
611
988
|
body.max_output_tokens = request.maxTokens;
|
|
989
|
+
if (structuredToolSchema) {
|
|
990
|
+
body.tools = [{ type: 'function', ...openAIStructuredOutputFunction(structuredToolSchema) }];
|
|
991
|
+
body.tool_choice = { type: 'function', name: OPENAI_STRUCTURED_OUTPUT_TOOL_NAME };
|
|
992
|
+
}
|
|
993
|
+
else if (request.responseFormat === 'json') {
|
|
994
|
+
body.text = {
|
|
995
|
+
format: request.responseSchema
|
|
996
|
+
? {
|
|
997
|
+
type: 'json_schema',
|
|
998
|
+
name: 'response',
|
|
999
|
+
strict: true,
|
|
1000
|
+
schema: toOpenAIStrictOutputSchema(request.responseSchema).schema,
|
|
1001
|
+
}
|
|
1002
|
+
: { type: 'json_object' },
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
612
1005
|
if (request.tools && request.tools.length > 0) {
|
|
1006
|
+
// `strict: false` keeps caller tools as they are on Chat Completions. With `strict` omitted,
|
|
1007
|
+
// Responses "will attempt to normalize your schema into strict mode when possible", and does
|
|
1008
|
+
// so by listing every property as required: an optional argument the user never gave
|
|
1009
|
+
// (`unit` of `get_weather`) came back filled in (`"unit": "celsius"`), and the app's input
|
|
1010
|
+
// check cannot tell it from a real one.
|
|
613
1011
|
body.tools = request.tools.map((tool) => ({
|
|
614
1012
|
type: 'function',
|
|
615
1013
|
name: tool.name,
|
|
616
1014
|
description: tool.description,
|
|
617
1015
|
parameters: tool.parameters,
|
|
1016
|
+
strict: false,
|
|
618
1017
|
}));
|
|
619
1018
|
const toolChoice = buildOpenAIResponsesToolChoice(request.toolChoice);
|
|
620
1019
|
if (toolChoice !== undefined)
|
|
@@ -622,24 +1021,36 @@ async function completeOpenAIResponses(args) {
|
|
|
622
1021
|
if (request.toolExecution?.parallelToolCalls === false)
|
|
623
1022
|
body.parallel_tool_calls = false;
|
|
624
1023
|
}
|
|
625
|
-
const
|
|
1024
|
+
const deadline = createRequestDeadline({
|
|
626
1025
|
providerName: 'openai',
|
|
627
1026
|
errorLabel: 'OpenAI Responses API',
|
|
628
|
-
|
|
1027
|
+
signal: request.signal,
|
|
1028
|
+
timeout: request.timeout,
|
|
1029
|
+
defaultTimeout: args.defaultTimeout,
|
|
1030
|
+
});
|
|
1031
|
+
const response = await executeRequestWithRetry({
|
|
1032
|
+
deadline,
|
|
1033
|
+
send: (transport) => fetch(`${args.baseUrl}/responses`, {
|
|
629
1034
|
method: 'POST',
|
|
630
1035
|
headers: {
|
|
1036
|
+
...request.transportHeaders,
|
|
631
1037
|
'Content-Type': 'application/json',
|
|
632
1038
|
Authorization: `Bearer ${args.apiKey}`,
|
|
633
1039
|
},
|
|
634
1040
|
body: JSON.stringify(body),
|
|
635
|
-
|
|
1041
|
+
...transport,
|
|
636
1042
|
}),
|
|
637
1043
|
});
|
|
638
|
-
const data = (await response
|
|
1044
|
+
const data = (await readResponseJson(response, deadline));
|
|
639
1045
|
const usage = openAIResponsesUsageToTokenUsage(data.usage);
|
|
640
1046
|
const output = data.output ?? [];
|
|
641
1047
|
const extracted = extractOpenAIResponsesContent(output);
|
|
642
|
-
const
|
|
1048
|
+
const structuredArguments = structuredToolSchema
|
|
1049
|
+
? output.find((item) => item.type === 'function_call' && item.name === OPENAI_STRUCTURED_OUTPUT_TOOL_NAME)?.arguments
|
|
1050
|
+
: undefined;
|
|
1051
|
+
const content = unwrapOpenAIStructuredContent(request, typeof structuredArguments === 'string' && structuredArguments.length > 0
|
|
1052
|
+
? structuredArguments
|
|
1053
|
+
: extracted.content || data.output_text || '');
|
|
643
1054
|
const responseModel = data.model ?? model;
|
|
644
1055
|
if (data.status === 'failed' || data.error) {
|
|
645
1056
|
throw new ProviderAPIError({
|
|
@@ -657,7 +1068,8 @@ async function completeOpenAIResponses(args) {
|
|
|
657
1068
|
usage,
|
|
658
1069
|
});
|
|
659
1070
|
}
|
|
660
|
-
|
|
1071
|
+
// The structured-output call is the answer, not a tool call for the caller to run.
|
|
1072
|
+
const toolCalls = structuredToolSchema ? [] : parseOpenAIResponsesToolCalls(output);
|
|
661
1073
|
if (toolCalls.length > 0) {
|
|
662
1074
|
return {
|
|
663
1075
|
content,
|
|
@@ -779,12 +1191,15 @@ export function createOpenAIAdapter(config) {
|
|
|
779
1191
|
model,
|
|
780
1192
|
messages,
|
|
781
1193
|
};
|
|
782
|
-
applyOpenAITemperature(body, model, request
|
|
783
|
-
applyOpenAIReasoning(body, request);
|
|
1194
|
+
applyOpenAITemperature(body, model, request);
|
|
1195
|
+
applyOpenAIReasoning(body, request, model);
|
|
784
1196
|
applyOpenAITokenLimit(body, model, request.maxTokens);
|
|
785
1197
|
if (request.tools && request.tools.length > 0) {
|
|
786
1198
|
assertNoStructuredOutputToolConflict(request);
|
|
787
1199
|
validateCallerTools(request.tools);
|
|
1200
|
+
const responseFormat = buildOpenAIResponseFormat(request);
|
|
1201
|
+
if (responseFormat)
|
|
1202
|
+
body.response_format = responseFormat;
|
|
788
1203
|
body.tools = request.tools.map((t) => ({
|
|
789
1204
|
type: 'function',
|
|
790
1205
|
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
@@ -817,25 +1232,32 @@ export function createOpenAIAdapter(config) {
|
|
|
817
1232
|
if (typeof request.seed === 'number') {
|
|
818
1233
|
body.seed = request.seed;
|
|
819
1234
|
}
|
|
820
|
-
const
|
|
1235
|
+
const deadline = createRequestDeadline({
|
|
821
1236
|
providerName: 'openai',
|
|
822
1237
|
errorLabel: 'OpenAI API',
|
|
823
|
-
|
|
1238
|
+
signal: request.signal,
|
|
1239
|
+
timeout: request.timeout,
|
|
1240
|
+
defaultTimeout,
|
|
1241
|
+
});
|
|
1242
|
+
const resp = await executeRequestWithRetry({
|
|
1243
|
+
deadline,
|
|
1244
|
+
send: (transport) => fetch(`${baseUrl}/chat/completions`, {
|
|
824
1245
|
method: 'POST',
|
|
825
1246
|
headers: {
|
|
1247
|
+
...request.transportHeaders,
|
|
826
1248
|
'Content-Type': 'application/json',
|
|
827
1249
|
Authorization: `Bearer ${config.apiKey}`,
|
|
828
1250
|
},
|
|
829
1251
|
body: JSON.stringify(body),
|
|
830
|
-
|
|
1252
|
+
...transport,
|
|
831
1253
|
}),
|
|
832
1254
|
});
|
|
833
|
-
const data = (await resp
|
|
1255
|
+
const data = (await readResponseJson(resp, deadline));
|
|
834
1256
|
const choice = data.choices[0];
|
|
835
1257
|
if (!choice)
|
|
836
1258
|
throw new Error('OpenAI returned no choices');
|
|
837
1259
|
const usage = openAIUsageToTokenUsage(data.usage);
|
|
838
|
-
const content = extractOpenAIMessageContent(choice.message);
|
|
1260
|
+
const content = unwrapOpenAIStructuredContent(request, extractOpenAIMessageContent(choice.message));
|
|
839
1261
|
if (choice.message.refusal) {
|
|
840
1262
|
throw new AIRefusalError({
|
|
841
1263
|
provider: 'openai',
|
|
@@ -899,8 +1321,8 @@ export function createOpenAIAdapter(config) {
|
|
|
899
1321
|
stream: true,
|
|
900
1322
|
stream_options: { include_usage: true },
|
|
901
1323
|
};
|
|
902
|
-
applyOpenAITemperature(body, model, request
|
|
903
|
-
applyOpenAIReasoning(body, request);
|
|
1324
|
+
applyOpenAITemperature(body, model, request);
|
|
1325
|
+
applyOpenAIReasoning(body, request, model);
|
|
904
1326
|
applyOpenAITokenLimit(body, model, request.maxTokens);
|
|
905
1327
|
const responseFormat = buildOpenAIResponseFormat(request);
|
|
906
1328
|
if (responseFormat) {
|
|
@@ -909,19 +1331,26 @@ export function createOpenAIAdapter(config) {
|
|
|
909
1331
|
if (typeof request.seed === 'number') {
|
|
910
1332
|
body.seed = request.seed;
|
|
911
1333
|
}
|
|
1334
|
+
const deadline = createRequestDeadline({
|
|
1335
|
+
providerName: 'openai',
|
|
1336
|
+
errorLabel: 'OpenAI API',
|
|
1337
|
+
signal: request.signal,
|
|
1338
|
+
timeout: request.timeout,
|
|
1339
|
+
defaultTimeout,
|
|
1340
|
+
});
|
|
912
1341
|
let resp;
|
|
913
1342
|
try {
|
|
914
1343
|
resp = await executeRequestWithRetry({
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
send: () => fetch(`${baseUrl}/chat/completions`, {
|
|
1344
|
+
deadline,
|
|
1345
|
+
send: (transport) => fetch(`${baseUrl}/chat/completions`, {
|
|
918
1346
|
method: 'POST',
|
|
919
1347
|
headers: {
|
|
1348
|
+
...request.transportHeaders,
|
|
920
1349
|
'Content-Type': 'application/json',
|
|
921
1350
|
Authorization: `Bearer ${config.apiKey}`,
|
|
922
1351
|
},
|
|
923
1352
|
body: JSON.stringify(body),
|
|
924
|
-
|
|
1353
|
+
...transport,
|
|
925
1354
|
}),
|
|
926
1355
|
});
|
|
927
1356
|
}
|
|
@@ -929,30 +1358,38 @@ export function createOpenAIAdapter(config) {
|
|
|
929
1358
|
yield {
|
|
930
1359
|
type: 'error',
|
|
931
1360
|
error: error instanceof Error ? error.message : String(error),
|
|
1361
|
+
cause: error,
|
|
932
1362
|
};
|
|
933
1363
|
return;
|
|
934
1364
|
}
|
|
935
|
-
|
|
1365
|
+
const events = parseSSEStream(resp, parseOpenAISSEChunk, deadline);
|
|
1366
|
+
yield* hasOpenAIStructuredEnvelope(request) ? unwrapOpenAIStructuredStream(events) : events;
|
|
936
1367
|
},
|
|
937
1368
|
async embed(request) {
|
|
938
1369
|
const body = {
|
|
939
1370
|
model: request.model ?? defaultEmbeddingModel,
|
|
940
1371
|
input: request.texts,
|
|
941
1372
|
};
|
|
942
|
-
const
|
|
1373
|
+
const deadline = createRequestDeadline({
|
|
943
1374
|
providerName: 'openai',
|
|
944
1375
|
errorLabel: 'OpenAI Embedding API',
|
|
945
|
-
|
|
1376
|
+
signal: request.signal,
|
|
1377
|
+
timeout: request.timeout,
|
|
1378
|
+
defaultTimeout,
|
|
1379
|
+
});
|
|
1380
|
+
const resp = await executeRequestWithRetry({
|
|
1381
|
+
deadline,
|
|
1382
|
+
send: (transport) => fetch(`${baseUrl}/embeddings`, {
|
|
946
1383
|
method: 'POST',
|
|
947
1384
|
headers: {
|
|
948
1385
|
'Content-Type': 'application/json',
|
|
949
1386
|
Authorization: `Bearer ${config.apiKey}`,
|
|
950
1387
|
},
|
|
951
1388
|
body: JSON.stringify(body),
|
|
952
|
-
|
|
1389
|
+
...transport,
|
|
953
1390
|
}),
|
|
954
1391
|
});
|
|
955
|
-
const data = (await resp
|
|
1392
|
+
const data = (await readResponseJson(resp, deadline));
|
|
956
1393
|
const totalTokens = data.usage.total_tokens;
|
|
957
1394
|
return {
|
|
958
1395
|
embeddings: data.data.map((d) => d.embedding),
|
|
@@ -1046,21 +1483,31 @@ export function createAnthropicAdapter(config) {
|
|
|
1046
1483
|
if (callerToolChoice !== undefined)
|
|
1047
1484
|
body.tool_choice = callerToolChoice;
|
|
1048
1485
|
}
|
|
1049
|
-
const
|
|
1486
|
+
const promptCache = resolvePromptCache(request.cache);
|
|
1487
|
+
if (promptCache)
|
|
1488
|
+
applyAnthropicPromptCache(body, promptCache);
|
|
1489
|
+
const deadline = createRequestDeadline({
|
|
1050
1490
|
providerName: 'anthropic',
|
|
1051
1491
|
errorLabel: 'Anthropic API',
|
|
1052
|
-
|
|
1492
|
+
signal: request.signal,
|
|
1493
|
+
timeout: request.timeout,
|
|
1494
|
+
defaultTimeout,
|
|
1495
|
+
});
|
|
1496
|
+
const resp = await executeRequestWithRetry({
|
|
1497
|
+
deadline,
|
|
1498
|
+
send: (transport) => fetch(`${baseUrl}/messages`, {
|
|
1053
1499
|
method: 'POST',
|
|
1054
1500
|
headers: {
|
|
1501
|
+
...request.transportHeaders,
|
|
1055
1502
|
'Content-Type': 'application/json',
|
|
1056
1503
|
'x-api-key': config.apiKey,
|
|
1057
1504
|
'anthropic-version': '2023-06-01',
|
|
1058
1505
|
},
|
|
1059
1506
|
body: JSON.stringify(body),
|
|
1060
|
-
|
|
1507
|
+
...transport,
|
|
1061
1508
|
}),
|
|
1062
1509
|
});
|
|
1063
|
-
const data = (await resp
|
|
1510
|
+
const data = (await readResponseJson(resp, deadline));
|
|
1064
1511
|
const usage = anthropicUsageToTokenUsage(data.usage);
|
|
1065
1512
|
const text = data.content
|
|
1066
1513
|
.filter((c) => c.type === 'text')
|
|
@@ -1108,11 +1555,7 @@ export function createAnthropicAdapter(config) {
|
|
|
1108
1555
|
};
|
|
1109
1556
|
},
|
|
1110
1557
|
async *stream(request) {
|
|
1111
|
-
|
|
1112
|
-
// Single-turn path: synthesize a single user message from `prompt`.
|
|
1113
|
-
const messages = request.messages && request.messages.length > 0
|
|
1114
|
-
? request.messages.map((m) => ({ role: m.role, content: m.content }))
|
|
1115
|
-
: [{ role: 'user', content: request.prompt }];
|
|
1558
|
+
const messages = toAnthropicMessages(request);
|
|
1116
1559
|
const body = {
|
|
1117
1560
|
model: request.model ?? defaultModel,
|
|
1118
1561
|
messages,
|
|
@@ -1131,20 +1574,30 @@ export function createAnthropicAdapter(config) {
|
|
|
1131
1574
|
};
|
|
1132
1575
|
}
|
|
1133
1576
|
applyAnthropicReasoning(body, request);
|
|
1577
|
+
const promptCache = resolvePromptCache(request.cache);
|
|
1578
|
+
if (promptCache)
|
|
1579
|
+
applyAnthropicPromptCache(body, promptCache);
|
|
1580
|
+
const deadline = createRequestDeadline({
|
|
1581
|
+
providerName: 'anthropic',
|
|
1582
|
+
errorLabel: 'Anthropic API',
|
|
1583
|
+
signal: request.signal,
|
|
1584
|
+
timeout: request.timeout,
|
|
1585
|
+
defaultTimeout,
|
|
1586
|
+
});
|
|
1134
1587
|
let resp;
|
|
1135
1588
|
try {
|
|
1136
1589
|
resp = await executeRequestWithRetry({
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
send: () => fetch(`${baseUrl}/messages`, {
|
|
1590
|
+
deadline,
|
|
1591
|
+
send: (transport) => fetch(`${baseUrl}/messages`, {
|
|
1140
1592
|
method: 'POST',
|
|
1141
1593
|
headers: {
|
|
1594
|
+
...request.transportHeaders,
|
|
1142
1595
|
'Content-Type': 'application/json',
|
|
1143
1596
|
'x-api-key': config.apiKey,
|
|
1144
1597
|
'anthropic-version': '2023-06-01',
|
|
1145
1598
|
},
|
|
1146
1599
|
body: JSON.stringify(body),
|
|
1147
|
-
|
|
1600
|
+
...transport,
|
|
1148
1601
|
}),
|
|
1149
1602
|
});
|
|
1150
1603
|
}
|
|
@@ -1152,11 +1605,12 @@ export function createAnthropicAdapter(config) {
|
|
|
1152
1605
|
yield {
|
|
1153
1606
|
type: 'error',
|
|
1154
1607
|
error: error instanceof Error ? error.message : String(error),
|
|
1608
|
+
cause: error,
|
|
1155
1609
|
};
|
|
1156
1610
|
return;
|
|
1157
1611
|
}
|
|
1158
1612
|
let initialUsage;
|
|
1159
|
-
for await (const event of parseSSEStream(resp, parseAnthropicSSEChunk)) {
|
|
1613
|
+
for await (const event of parseSSEStream(resp, parseAnthropicSSEChunk, deadline)) {
|
|
1160
1614
|
if (event.type === 'usage' && event.usage)
|
|
1161
1615
|
initialUsage = event.usage;
|
|
1162
1616
|
if (event.type === 'done' && event.usage && initialUsage) {
|
|
@@ -1230,7 +1684,7 @@ export function createAnthropicAdapter(config) {
|
|
|
1230
1684
|
},
|
|
1231
1685
|
};
|
|
1232
1686
|
}
|
|
1233
|
-
async function* parseSSEStream(resp, parseChunk) {
|
|
1687
|
+
async function* parseSSEStream(resp, parseChunk, deadline) {
|
|
1234
1688
|
if (!resp.body) {
|
|
1235
1689
|
yield { type: 'error', error: 'No response body for streaming' };
|
|
1236
1690
|
return;
|
|
@@ -1241,7 +1695,10 @@ async function* parseSSEStream(resp, parseChunk) {
|
|
|
1241
1695
|
let currentEvent = '';
|
|
1242
1696
|
try {
|
|
1243
1697
|
while (true) {
|
|
1244
|
-
|
|
1698
|
+
// The request's timeout covers the whole stream: a cut-off read fails as that timeout.
|
|
1699
|
+
const { done, value } = await reader.read().catch((error) => {
|
|
1700
|
+
throw deadline.classify(error);
|
|
1701
|
+
});
|
|
1245
1702
|
if (done)
|
|
1246
1703
|
break;
|
|
1247
1704
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -1282,15 +1739,7 @@ function parseOpenAISSEChunk(_eventType, data) {
|
|
|
1282
1739
|
// Usage-only chunk (sent when stream_options.include_usage is true)
|
|
1283
1740
|
if (parsed.usage &&
|
|
1284
1741
|
(!parsed.choices || parsed.choices.length === 0 || !parsed.choices[0]?.delta?.content)) {
|
|
1285
|
-
return {
|
|
1286
|
-
type: 'usage',
|
|
1287
|
-
usage: {
|
|
1288
|
-
inputTokens: parsed.usage.prompt_tokens,
|
|
1289
|
-
outputTokens: parsed.usage.completion_tokens,
|
|
1290
|
-
totalTokens: parsed.usage.total_tokens,
|
|
1291
|
-
cachedInputTokens: parsed.usage.prompt_tokens_details?.cached_tokens ?? 0,
|
|
1292
|
-
},
|
|
1293
|
-
};
|
|
1742
|
+
return { type: 'usage', usage: openAIUsageToTokenUsage(parsed.usage) };
|
|
1294
1743
|
}
|
|
1295
1744
|
const choice = parsed.choices?.[0];
|
|
1296
1745
|
if (!choice)
|