@salesforce/sfdx-agent-harness-mastra 0.100.0 → 0.101.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/CHANGELOG.md +5 -0
- package/README.md +5 -11
- package/dist/index.d.ts +5 -5
- package/dist/mastra-harness-factory.d.ts +3 -3
- package/dist/mastra-harness.d.ts +6 -6
- package/dist/mastra-harness.js +7 -7
- package/dist/pass-through-anthropic.d.ts +15 -15
- package/dist/pass-through-anthropic.js +21 -23
- package/dist/pass-through-openai-chat-completions.d.ts +12 -12
- package/dist/pass-through-openai-chat-completions.js +15 -17
- package/dist/pass-through-openai.d.ts +9 -9
- package/dist/pass-through-openai.js +14 -16
- package/dist/retry-pre-first-token.d.ts +2 -2
- package/dist/stream-errors.d.ts +1 -1
- package/dist/stream-errors.js +1 -1
- package/dist/{v3-stream-mapping.d.ts → stream-mapping.d.ts} +62 -47
- package/dist/{v3-stream-mapping.js → stream-mapping.js} +51 -32
- package/dist/tool-args-marker.d.ts +1 -1
- package/dist/tool-args-marker.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
All notable changes to `@salesforce/sfdx-agent-harness-mastra` are documented in this file.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.101.0] - 2026-10-13
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **harness-mastra**: migrate pass-throughs to LanguageModelV4 and remove chat-generations @W-24224624@ ([#876](https://github.com/forcedotcom/agentic-dx/pull/876))
|
|
10
|
+
|
|
6
11
|
## [0.100.0] - 2026-10-09
|
|
7
12
|
|
|
8
13
|
### Chores
|
package/README.md
CHANGED
|
@@ -59,8 +59,8 @@ Linux and macOS release the handle eagerly; the retry options are no-ops on thos
|
|
|
59
59
|
| ----------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
60
60
|
| `MastraHarnessFactory` | class | `HarnessFactory` that produces a Mastra-backed `AgentHarness` bound to a storage root. |
|
|
61
61
|
| `MastraHarnessFactoryConfig` | interface | Configuration for `MastraHarnessFactory` (`toolApprovalTimeoutMs?`, `languageModelBuilders?`). |
|
|
62
|
-
| `MastraLanguageModelBuilderContext` | type | Context bag the harness threads into each `
|
|
63
|
-
| `MastraLanguageModelBuilder` | type | Factory function shape for a consumer-injected builder: `(ctx: MastraLanguageModelBuilderContext) =>
|
|
62
|
+
| `MastraLanguageModelBuilderContext` | type | Context bag the harness threads into each `LanguageModelV4` builder (`getInfo`, `logBus`, `emitWireCommunication`, `innerFetch`). |
|
|
63
|
+
| `MastraLanguageModelBuilder` | type | Factory function shape for a consumer-injected builder: `(ctx: MastraLanguageModelBuilderContext) => LanguageModelV4`. |
|
|
64
64
|
| `MASTRA_BUILT_IN_TOOL_POLICIES` | const | Frozen `ReadonlyArray<ToolPolicyRule>` the harness feeds the SDK's `resolveToolApprovalPolicy` as the `tiers.harness` slice — `allow` rules for Mastra's built-in `updateWorkingMemory` / `skill` / `skill_search` / `skill_read` tools. |
|
|
65
65
|
| `MASTRA_BUILT_IN_TOOL_NAMES` | const | The four built-in tool leaf names `MASTRA_BUILT_IN_TOOL_POLICIES` covers. |
|
|
66
66
|
|
|
@@ -81,20 +81,14 @@ no `globalThis` mutation. When no proxy env is set, `fetch` runs unwrapped (zero
|
|
|
81
81
|
|
|
82
82
|
#### Injecting custom wire shapes
|
|
83
83
|
|
|
84
|
-
A consumer can register a `
|
|
84
|
+
A consumer can register a `LanguageModelV4` builder for a custom `providerHint` via
|
|
85
85
|
`MastraHarnessFactoryConfig.languageModelBuilders` without editing the harness's internal switch. Each injected key
|
|
86
|
-
becomes a supported provider hint and a dispatch arm. Used to inject wire shapes the harness does not ship natively
|
|
87
|
-
(e.g., a temporary GovCloud `chat/generations` fallback).
|
|
86
|
+
becomes a supported provider hint and a dispatch arm. Used to inject wire shapes the harness does not ship natively.
|
|
88
87
|
|
|
89
88
|
```ts
|
|
90
|
-
import {
|
|
91
|
-
CHAT_GENERATIONS_PROVIDER_HINT,
|
|
92
|
-
buildChatGenerationsLanguageModel,
|
|
93
|
-
} from '@salesforce/sfdx-agent-chat-generations';
|
|
94
|
-
|
|
95
89
|
const factory = new MastraHarnessFactory({
|
|
96
90
|
languageModelBuilders: {
|
|
97
|
-
|
|
91
|
+
'my-custom-wire-shape': (ctx) => buildMyLanguageModel(ctx),
|
|
98
92
|
},
|
|
99
93
|
});
|
|
100
94
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { LogBus } from '@salesforce/agentic-common';
|
|
2
2
|
import type { ModelConnectivityInfo, WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
|
|
3
|
-
import type {
|
|
3
|
+
import type { LanguageModelV4 } from '@ai-sdk/provider';
|
|
4
4
|
export { MastraHarnessFactory, type MastraHarnessFactoryConfig } from './mastra-harness-factory.js';
|
|
5
5
|
export type { MastraAgentHarness } from './mastra-harness.js';
|
|
6
6
|
export { MASTRA_BUILT_IN_TOOL_NAMES, MASTRA_BUILT_IN_TOOL_POLICIES } from './mastra-built-in-policies.js';
|
|
7
7
|
export { type MastraAgentConfig, type MastraSkillSearchConfig, type MastraToolSearchConfig, type SkillSearchHandle, type ToolSearchHandle, } from './tool-search.js';
|
|
8
8
|
/**
|
|
9
|
-
* Context the harness threads into each `
|
|
9
|
+
* Context the harness threads into each `LanguageModelV4` builder — both the
|
|
10
10
|
* built-in builders and any consumer-injected builders registered via
|
|
11
11
|
* `MastraHarnessFactoryConfig.languageModelBuilders`. The shape mirrors what
|
|
12
12
|
* `MastraHarness.buildLanguageModel` currently hands its internal builders, so an
|
|
@@ -33,10 +33,10 @@ export type MastraLanguageModelBuilderContext = {
|
|
|
33
33
|
innerFetch: typeof fetch;
|
|
34
34
|
};
|
|
35
35
|
/**
|
|
36
|
-
* Factory function shape for a consumer-injected `
|
|
36
|
+
* Factory function shape for a consumer-injected `LanguageModelV4` builder.
|
|
37
37
|
* Registered via `MastraHarnessFactoryConfig.languageModelBuilders` keyed by a custom
|
|
38
38
|
* `providerHint`, used to inject wire shapes the harness does not ship natively
|
|
39
|
-
* (e.g. a
|
|
39
|
+
* (e.g. a bespoke gateway wire format). The builder receives the
|
|
40
40
|
* same context bag the harness's built-in pass-through builders get, so an injected
|
|
41
41
|
* builder can emit `WireCommunicationEvent`s and honor `HTTPS_PROXY` routing without
|
|
42
42
|
* duplicating the harness's own fetch wrapper / wire-monitoring plumbing. The
|
|
@@ -55,4 +55,4 @@ export type MastraLanguageModelBuilderContext = {
|
|
|
55
55
|
* });
|
|
56
56
|
* ```
|
|
57
57
|
*/
|
|
58
|
-
export type MastraLanguageModelBuilder = (ctx: MastraLanguageModelBuilderContext) =>
|
|
58
|
+
export type MastraLanguageModelBuilder = (ctx: MastraLanguageModelBuilderContext) => LanguageModelV4;
|
|
@@ -12,10 +12,10 @@ export interface MastraHarnessFactoryConfig {
|
|
|
12
12
|
*/
|
|
13
13
|
toolApprovalTimeoutMs?: number;
|
|
14
14
|
/**
|
|
15
|
-
* Consumer-registered `
|
|
15
|
+
* Consumer-registered `LanguageModelV4` builders keyed by `providerHint`. Each key
|
|
16
16
|
* becomes a supported provider hint and a dispatch arm in `buildLanguageModel`. Used
|
|
17
|
-
* to inject wire shapes the harness does not ship natively (e.g. a
|
|
18
|
-
*
|
|
17
|
+
* to inject wire shapes the harness does not ship natively (e.g. a bespoke
|
|
18
|
+
* gateway wire format). Keys MUST NOT collide with the
|
|
19
19
|
* harness's built-in hints (`anthropic`, `bedrock-anthropic`, `openai`,
|
|
20
20
|
* `openai-responses`, `openai-compatible`) — constructing with a colliding key
|
|
21
21
|
* throws at factory construction time rather than silently shadowing the built-in.
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { MastraApprovalCoordinator } from './mastra-approval-coordinator.js';
|
|
|
8
8
|
import { type HttpErrorCapture } from './mcp-config-mapper.js';
|
|
9
9
|
import { type MastraAgentConfig, type SkillSearchHandle, type ToolSearchHandle } from './tool-search.js';
|
|
10
10
|
import { type MultimodalFile } from '@salesforce/sfdx-agent-sdk';
|
|
11
|
-
import type {
|
|
11
|
+
import type { LanguageModelV4 } from '@ai-sdk/provider';
|
|
12
12
|
import { Clock, type LogRecord, type BackoffRetryerDeps, type UniqueIDGenerator, type Unsubscribe } from '@salesforce/agentic-common';
|
|
13
13
|
import { type ToolsInput } from './utils.js';
|
|
14
14
|
import { Workspace } from '@mastra/core/workspace';
|
|
@@ -247,13 +247,13 @@ export interface MastraHarnessOptions {
|
|
|
247
247
|
*/
|
|
248
248
|
innerFetch?: typeof fetch;
|
|
249
249
|
/**
|
|
250
|
-
* Consumer-registered `
|
|
250
|
+
* Consumer-registered `LanguageModelV4` builders keyed by `providerHint`.
|
|
251
251
|
* Forwarded from `MastraHarnessFactoryConfig.languageModelBuilders`. The
|
|
252
252
|
* harness consults this map in `buildLanguageModel`'s `default` arm before
|
|
253
253
|
* throwing `MODEL_NOT_SUPPORTED_BY_HARNESS`, so an injected builder can
|
|
254
254
|
* handle a custom hint without editing the harness's internal switch.
|
|
255
255
|
*/
|
|
256
|
-
languageModelBuilders?: Readonly<Record<string, (ctx: MastraLanguageModelBuilderContext) =>
|
|
256
|
+
languageModelBuilders?: Readonly<Record<string, (ctx: MastraLanguageModelBuilderContext) => LanguageModelV4>>;
|
|
257
257
|
/**
|
|
258
258
|
* Clock / sleep / RNG for the pre-first-token LLM retry loop (W-24236393), whose policy is
|
|
259
259
|
* `PRE_FIRST_TOKEN_RETRY_OPTIONS`. Tests inject a no-sleep `sleep`.
|
|
@@ -514,7 +514,7 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
514
514
|
* non-empty paths.
|
|
515
515
|
*/
|
|
516
516
|
/**
|
|
517
|
-
* Dispatches on the bag's `providerHint` and returns a `
|
|
517
|
+
* Dispatches on the bag's `providerHint` and returns a `LanguageModelV4` that
|
|
518
518
|
* speaks the provider-native pass-through endpoint. The builder closes over
|
|
519
519
|
* `getInfo` so within-shape `modelId` swaps, JWT rotations, and
|
|
520
520
|
* premium-rate-limit gate flips land on the next outbound call without
|
|
@@ -588,8 +588,8 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
588
588
|
* `agent.stream()` accepts.
|
|
589
589
|
*
|
|
590
590
|
* `text` parts collapse into text content blocks; `image` / `file` parts become v5 `file`
|
|
591
|
-
* content blocks (`mediaType` + base64 `data` + optional `filename`) that Mastra
|
|
592
|
-
*
|
|
591
|
+
* content blocks (`mediaType` + base64 `data` + optional `filename`) that Mastra normalizes to V4
|
|
592
|
+
* `file` parts for the provider pass-through translators. Pre-stream validation and the
|
|
593
593
|
* input-part guard live in the SDK's {@link lowerStreamInput}, so file caps and the
|
|
594
594
|
* `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics are identical to the Claude
|
|
595
595
|
* harness; this method only supplies the Mastra-specific lowering of each part.
|
package/dist/mastra-harness.js
CHANGED
|
@@ -27,7 +27,7 @@ import { validateMultimodalFiles } from '@salesforce/sfdx-agent-sdk';
|
|
|
27
27
|
import { buildAnthropicDirectLanguageModel, buildAnthropicLanguageModel } from './pass-through-anthropic.js';
|
|
28
28
|
import { buildOpenAIChatCompletionsLanguageModel } from './pass-through-openai-chat-completions.js';
|
|
29
29
|
import { buildOpenAILanguageModel } from './pass-through-openai.js';
|
|
30
|
-
import { readToolArgsRetryScope, TOOL_ARGS_RETRY_SCOPE_AGENT_ID, TOOL_ARGS_RETRY_SCOPE_PROVIDER, TOOL_ARGS_RETRY_SCOPE_THREAD_ID, } from './
|
|
30
|
+
import { readToolArgsRetryScope, TOOL_ARGS_RETRY_SCOPE_AGENT_ID, TOOL_ARGS_RETRY_SCOPE_PROVIDER, TOOL_ARGS_RETRY_SCOPE_THREAD_ID, } from './stream-mapping.js';
|
|
31
31
|
import { ToolResultRedactionProcessor } from './mastra-tool-result-redaction-processor.js';
|
|
32
32
|
import { backfillCreatedAt, buildSummaryPrompt, Clock, getErrorMessage, RealClock, splitFrontmatterAndBody, UUIDGenerator, } from '@salesforce/agentic-common';
|
|
33
33
|
import { withPreFirstTokenRetry } from './retry-pre-first-token.js';
|
|
@@ -1550,7 +1550,7 @@ export class MastraHarness {
|
|
|
1550
1550
|
* non-empty paths.
|
|
1551
1551
|
*/
|
|
1552
1552
|
/**
|
|
1553
|
-
* Dispatches on the bag's `providerHint` and returns a `
|
|
1553
|
+
* Dispatches on the bag's `providerHint` and returns a `LanguageModelV4` that
|
|
1554
1554
|
* speaks the provider-native pass-through endpoint. The builder closes over
|
|
1555
1555
|
* `getInfo` so within-shape `modelId` swaps, JWT rotations, and
|
|
1556
1556
|
* premium-rate-limit gate flips land on the next outbound call without
|
|
@@ -1671,9 +1671,9 @@ export class MastraHarness {
|
|
|
1671
1671
|
}));
|
|
1672
1672
|
default: {
|
|
1673
1673
|
// Consult the injected builders map before throwing. This is the
|
|
1674
|
-
// extension point for consumer-registered wire shapes (e.g. a
|
|
1675
|
-
//
|
|
1676
|
-
//
|
|
1674
|
+
// extension point for consumer-registered wire shapes (e.g. a bespoke
|
|
1675
|
+
// gateway wire format). The builder receives the same context bag the
|
|
1676
|
+
// built-in builders get.
|
|
1677
1677
|
const custom = this.languageModelBuilders?.[initial.providerHint];
|
|
1678
1678
|
if (custom) {
|
|
1679
1679
|
return this.wrapLanguageModel(custom({ getInfo, logBus, emitWireCommunication, innerFetch }));
|
|
@@ -1773,8 +1773,8 @@ export class MastraHarness {
|
|
|
1773
1773
|
* `agent.stream()` accepts.
|
|
1774
1774
|
*
|
|
1775
1775
|
* `text` parts collapse into text content blocks; `image` / `file` parts become v5 `file`
|
|
1776
|
-
* content blocks (`mediaType` + base64 `data` + optional `filename`) that Mastra
|
|
1777
|
-
*
|
|
1776
|
+
* content blocks (`mediaType` + base64 `data` + optional `filename`) that Mastra normalizes to V4
|
|
1777
|
+
* `file` parts for the provider pass-through translators. Pre-stream validation and the
|
|
1778
1778
|
* input-part guard live in the SDK's {@link lowerStreamInput}, so file caps and the
|
|
1779
1779
|
* `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics are identical to the Claude
|
|
1780
1780
|
* harness; this method only supplies the Mastra-specific lowering of each part.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
-
import type {
|
|
2
|
+
import type { LanguageModelV4, LanguageModelV4CallOptions, LanguageModelV4Prompt } from '@ai-sdk/provider';
|
|
3
3
|
import type { LogBus } from '@salesforce/agentic-common';
|
|
4
4
|
import type { ModelConnectivityInfo, WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
|
|
5
|
-
import { type GatewayFetchWireHooks } from './
|
|
5
|
+
import { type GatewayFetchWireHooks } from './stream-mapping.js';
|
|
6
6
|
/**
|
|
7
|
-
* Builds the in-process `
|
|
7
|
+
* Builds the in-process `LanguageModelV4` impl for the Salesforce LLM Gateway's
|
|
8
8
|
* `/invoke-with-response-stream` (Bedrock-Anthropic pass-through) endpoint.
|
|
9
9
|
* Drives `@anthropic-ai/bedrock-sdk` with `skipAuth: true` so the connectivity
|
|
10
10
|
* resolver's headers (Salesforce JWT, tenant key, etc.) are the single source
|
|
@@ -66,12 +66,12 @@ export declare function buildAnthropicLanguageModel(opts: {
|
|
|
66
66
|
* `LanguageModel`. Defaults to a single per-instance map for callers that
|
|
67
67
|
* don't thread it (tests / consumer-injected builders).
|
|
68
68
|
*/
|
|
69
|
-
getToolArgsRetryCounts?: (options:
|
|
69
|
+
getToolArgsRetryCounts?: (options: LanguageModelV4CallOptions) => Map<string, number>;
|
|
70
70
|
/** Per-read idle timeout (ms) for the gateway response body; see `createGatewayFetch`. */
|
|
71
71
|
idleReadTimeoutMs?: number;
|
|
72
|
-
}):
|
|
72
|
+
}): LanguageModelV4;
|
|
73
73
|
/**
|
|
74
|
-
* Builds the in-process `
|
|
74
|
+
* Builds the in-process `LanguageModelV4` impl for direct-Anthropic API access
|
|
75
75
|
* (`api.anthropic.com/v1/messages` or any Anthropic-shaped endpoint). Drives
|
|
76
76
|
* `@anthropic-ai/sdk` with the api-key peeled from the resolver's headers.
|
|
77
77
|
*
|
|
@@ -101,10 +101,10 @@ export declare function buildAnthropicDirectLanguageModel(opts: {
|
|
|
101
101
|
/** See {@link buildAnthropicLanguageModel} `getConsumerToolSchemas`. */
|
|
102
102
|
getConsumerToolSchemas?: () => ReadonlyMap<string, Record<string, unknown>>;
|
|
103
103
|
/** See {@link buildAnthropicLanguageModel} `getToolArgsRetryCounts`. */
|
|
104
|
-
getToolArgsRetryCounts?: (options:
|
|
104
|
+
getToolArgsRetryCounts?: (options: LanguageModelV4CallOptions) => Map<string, number>;
|
|
105
105
|
/** Per-read idle timeout (ms) for the gateway response body; see `createGatewayFetch`. */
|
|
106
106
|
idleReadTimeoutMs?: number;
|
|
107
|
-
}):
|
|
107
|
+
}): LanguageModelV4;
|
|
108
108
|
/**
|
|
109
109
|
* Stamps `cache_control: {type:'ephemeral'}` on up to three of Anthropic's four
|
|
110
110
|
* allowed breakpoints: the last tool (caches the constant tools block), the
|
|
@@ -115,25 +115,25 @@ export declare function buildAnthropicDirectLanguageModel(opts: {
|
|
|
115
115
|
* only the turn-constant tools/system cached and forfeit most of the win.
|
|
116
116
|
* Mutates only `commonParams`'s own arrays — which
|
|
117
117
|
* `translate*ToAnthropic` builds fresh per request — never the caller's
|
|
118
|
-
* original `
|
|
118
|
+
* original `LanguageModelV4` prompt/tool objects.
|
|
119
119
|
*/
|
|
120
120
|
export declare function applyCacheBreakpoints(commonParams: Omit<Anthropic.Messages.MessageCreateParamsStreaming, 'stream'>): void;
|
|
121
121
|
/**
|
|
122
|
-
* Translates a
|
|
122
|
+
* Translates a V4 `LanguageModelV4Prompt` into Anthropic's Messages API shape
|
|
123
123
|
* — top-level `system` string + `MessageParam[]` of role-tagged content blocks.
|
|
124
124
|
*
|
|
125
|
-
*
|
|
125
|
+
* V4 keeps system / user / assistant / tool messages flat. Anthropic combines
|
|
126
126
|
* tool results onto a synthetic user-role turn carrying `tool_result` blocks,
|
|
127
|
-
* so we collapse consecutive tool-only
|
|
127
|
+
* so we collapse consecutive tool-only V4 messages into the surrounding user
|
|
128
128
|
* turn shape.
|
|
129
129
|
*/
|
|
130
|
-
export declare function translatePromptToAnthropic(prompt:
|
|
130
|
+
export declare function translatePromptToAnthropic(prompt: LanguageModelV4Prompt): {
|
|
131
131
|
system?: string;
|
|
132
132
|
messages: Anthropic.Messages.MessageParam[];
|
|
133
133
|
};
|
|
134
134
|
/**
|
|
135
|
-
* Translates
|
|
135
|
+
* Translates V4 function-tool definitions into Anthropic's `Tool[]` shape.
|
|
136
136
|
* Skips non-function variants (provider-defined / dynamic) — the pass-through
|
|
137
137
|
* path supports only the function-tool subset that Mastra's tool mapper produces.
|
|
138
138
|
*/
|
|
139
|
-
export declare function translateToolsToAnthropic(options:
|
|
139
|
+
export declare function translateToolsToAnthropic(options: LanguageModelV4CallOptions): Anthropic.Messages.Tool[];
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import { AnthropicBedrock } from '@anthropic-ai/bedrock-sdk';
|
|
6
6
|
import Anthropic from '@anthropic-ai/sdk';
|
|
7
7
|
import { EmptyStreamError, NetworkStreamError, isStreamTransportError, recognizeBedrockValidationError, } from './stream-errors.js';
|
|
8
|
-
import { SpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapAnthropicFinishReason, mapAnthropicUsage, toolResultOutputToString, } from './
|
|
8
|
+
import { SpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapAnthropicFinishReason, mapAnthropicUsage, toolResultOutputToString, v4FileDataToBase64, } from './stream-mapping.js';
|
|
9
9
|
/**
|
|
10
|
-
* Builds the in-process `
|
|
10
|
+
* Builds the in-process `LanguageModelV4` impl for the Salesforce LLM Gateway's
|
|
11
11
|
* `/invoke-with-response-stream` (Bedrock-Anthropic pass-through) endpoint.
|
|
12
12
|
* Drives `@anthropic-ai/bedrock-sdk` with `skipAuth: true` so the connectivity
|
|
13
13
|
* resolver's headers (Salesforce JWT, tenant key, etc.) are the single source
|
|
@@ -44,7 +44,7 @@ export function buildAnthropicLanguageModel(opts) {
|
|
|
44
44
|
}),
|
|
45
45
|
};
|
|
46
46
|
return {
|
|
47
|
-
specificationVersion: '
|
|
47
|
+
specificationVersion: 'v4',
|
|
48
48
|
provider: 'salesforce-llm-gateway-pass-through-anthropic',
|
|
49
49
|
// Mastra captures `modelId` once at construction. The actual wire id
|
|
50
50
|
// sent on every call is read from the live bag via `commonParams.model`
|
|
@@ -57,7 +57,7 @@ export function buildAnthropicLanguageModel(opts) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
|
-
* Builds the in-process `
|
|
60
|
+
* Builds the in-process `LanguageModelV4` impl for direct-Anthropic API access
|
|
61
61
|
* (`api.anthropic.com/v1/messages` or any Anthropic-shaped endpoint). Drives
|
|
62
62
|
* `@anthropic-ai/sdk` with the api-key peeled from the resolver's headers.
|
|
63
63
|
*
|
|
@@ -104,7 +104,7 @@ export function buildAnthropicDirectLanguageModel(opts) {
|
|
|
104
104
|
}),
|
|
105
105
|
};
|
|
106
106
|
return {
|
|
107
|
-
specificationVersion: '
|
|
107
|
+
specificationVersion: 'v4',
|
|
108
108
|
provider: 'anthropic-direct-pass-through',
|
|
109
109
|
modelId: initialInfo.nativeModelId,
|
|
110
110
|
supportedUrls: {},
|
|
@@ -231,7 +231,7 @@ const CACHE_CONTROL_EPHEMERAL = Object.freeze({ type: 'ephemeral' });
|
|
|
231
231
|
* only the turn-constant tools/system cached and forfeit most of the win.
|
|
232
232
|
* Mutates only `commonParams`'s own arrays — which
|
|
233
233
|
* `translate*ToAnthropic` builds fresh per request — never the caller's
|
|
234
|
-
* original `
|
|
234
|
+
* original `LanguageModelV4` prompt/tool objects.
|
|
235
235
|
*/
|
|
236
236
|
export function applyCacheBreakpoints(commonParams) {
|
|
237
237
|
const tools = commonParams.tools;
|
|
@@ -348,7 +348,7 @@ async function doGenerate(ctx, options) {
|
|
|
348
348
|
content.push(...buildValidatedToolCallChunk(block.id, block.name, argsBuffer, toolSchemas.get(block.name), ctx.getConsumerToolNames().has(block.name), { retryCounts: ctx.getToolArgsRetryCounts(options), logBus: ctx.logBus }));
|
|
349
349
|
}
|
|
350
350
|
// Other block types (redacted_thinking, server-tool-result, etc.) are
|
|
351
|
-
// not in the
|
|
351
|
+
// not in the V4 ChatEvent surface — same posture as doStream.
|
|
352
352
|
}
|
|
353
353
|
const finishReason = mapAnthropicFinishReason(message.stop_reason, sawToolCalls);
|
|
354
354
|
const usage = mapAnthropicUsage(message.usage);
|
|
@@ -398,7 +398,7 @@ async function doStream(ctx, options) {
|
|
|
398
398
|
const stream = new ReadableStream({
|
|
399
399
|
start: async (controller) => {
|
|
400
400
|
const textSpan = new SpanLifecycle();
|
|
401
|
-
// Extended-thinking narration streams on its own
|
|
401
|
+
// Extended-thinking narration streams on its own V4 reasoning span,
|
|
402
402
|
// so it surfaces as `reasoning-delta` (→ AG-UI REASONING_MESSAGE_CONTENT)
|
|
403
403
|
// rather than bleeding into the answer's `text-delta`.
|
|
404
404
|
const reasoningSpan = new SpanLifecycle('reasoning');
|
|
@@ -432,7 +432,7 @@ async function doStream(ctx, options) {
|
|
|
432
432
|
case 'content_block_start': {
|
|
433
433
|
const block = event.content_block;
|
|
434
434
|
if (block.type === 'text') {
|
|
435
|
-
// Open the
|
|
435
|
+
// Open the V4 text span on the first text block of the message
|
|
436
436
|
// (or after a tool_use rotated us forward).
|
|
437
437
|
textSpan.openIfNeeded(controller);
|
|
438
438
|
}
|
|
@@ -460,7 +460,7 @@ async function doStream(ctx, options) {
|
|
|
460
460
|
});
|
|
461
461
|
}
|
|
462
462
|
// Other block types (redacted_thinking, server-tool result, etc.)
|
|
463
|
-
// are intentionally ignored — they're not in the
|
|
463
|
+
// are intentionally ignored — they're not in the V4 ChatEvent surface.
|
|
464
464
|
break;
|
|
465
465
|
}
|
|
466
466
|
case 'content_block_delta': {
|
|
@@ -481,7 +481,7 @@ async function doStream(ctx, options) {
|
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
483
|
// signature_delta / citations_delta are not surfaced — the
|
|
484
|
-
// encrypted thinking signature has no
|
|
484
|
+
// encrypted thinking signature has no V4 stream-part and no
|
|
485
485
|
// consumer surface (AG-UI ReasoningEncryptedValue is deferred).
|
|
486
486
|
break;
|
|
487
487
|
}
|
|
@@ -526,7 +526,7 @@ async function doStream(ctx, options) {
|
|
|
526
526
|
}
|
|
527
527
|
// Stream ended without an explicit `message_stop`. If we saw at least
|
|
528
528
|
// one recognized content / usage event, close out the span and
|
|
529
|
-
// synthesize a terminal `finish` so
|
|
529
|
+
// synthesize a terminal `finish` so V4 consumers see exactly one. If
|
|
530
530
|
// we saw NONE — a clean-but-empty body — surface a classified `error`
|
|
531
531
|
// instead of a false-success empty `finish` (8a). A cancelled turn is
|
|
532
532
|
// exempt: an aborted stream ends cleanly here too (the SDK `Stream`
|
|
@@ -556,7 +556,7 @@ async function doStream(ctx, options) {
|
|
|
556
556
|
// Trailing-JSON #396: the gateway sometimes appends a JSON error envelope
|
|
557
557
|
// after the terminal eventstream frame on otherwise-successful responses,
|
|
558
558
|
// which the Anthropic SDK's parser surfaces as a late throw. Drop it
|
|
559
|
-
// silently —
|
|
559
|
+
// silently — V4 consumers must see exactly one terminal `finish`.
|
|
560
560
|
controller.close();
|
|
561
561
|
return;
|
|
562
562
|
}
|
|
@@ -574,7 +574,7 @@ async function doStream(ctx, options) {
|
|
|
574
574
|
});
|
|
575
575
|
return { stream };
|
|
576
576
|
}
|
|
577
|
-
/** Default Anthropic `max_tokens` when
|
|
577
|
+
/** Default Anthropic `max_tokens` when V4 doesn't supply one. The Bedrock invoke
|
|
578
578
|
* API requires the field; Mastra leaves it unset for many turns. 4096 mirrors
|
|
579
579
|
* the `@ai-sdk/anthropic` provider's default. */
|
|
580
580
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
@@ -590,12 +590,12 @@ const DEFAULT_MAX_TOKENS = 4096;
|
|
|
590
590
|
*/
|
|
591
591
|
const SUMMARY_MAX_OUTPUT_TOKENS = 16384;
|
|
592
592
|
/**
|
|
593
|
-
* Translates a
|
|
593
|
+
* Translates a V4 `LanguageModelV4Prompt` into Anthropic's Messages API shape
|
|
594
594
|
* — top-level `system` string + `MessageParam[]` of role-tagged content blocks.
|
|
595
595
|
*
|
|
596
|
-
*
|
|
596
|
+
* V4 keeps system / user / assistant / tool messages flat. Anthropic combines
|
|
597
597
|
* tool results onto a synthetic user-role turn carrying `tool_result` blocks,
|
|
598
|
-
* so we collapse consecutive tool-only
|
|
598
|
+
* so we collapse consecutive tool-only V4 messages into the surrounding user
|
|
599
599
|
* turn shape.
|
|
600
600
|
*/
|
|
601
601
|
export function translatePromptToAnthropic(prompt) {
|
|
@@ -669,7 +669,7 @@ export function translatePromptToAnthropic(prompt) {
|
|
|
669
669
|
return system !== undefined ? { system, messages } : { messages };
|
|
670
670
|
}
|
|
671
671
|
/**
|
|
672
|
-
* Translates
|
|
672
|
+
* Translates V4 function-tool definitions into Anthropic's `Tool[]` shape.
|
|
673
673
|
* Skips non-function variants (provider-defined / dynamic) — the pass-through
|
|
674
674
|
* path supports only the function-tool subset that Mastra's tool mapper produces.
|
|
675
675
|
*/
|
|
@@ -685,13 +685,11 @@ export function translateToolsToAnthropic(options) {
|
|
|
685
685
|
}));
|
|
686
686
|
}
|
|
687
687
|
function toAnthropicFileBlock(part) {
|
|
688
|
-
|
|
689
|
-
throw new Error(`Unsupported file data for "${part.mediaType}": expected a base64 string, got ${part.data instanceof URL ? 'a URL' : 'binary data'}. Only base64-encoded file data is supported.`);
|
|
690
|
-
}
|
|
688
|
+
const base64 = v4FileDataToBase64(part.data, part.mediaType);
|
|
691
689
|
if (part.mediaType === 'application/pdf') {
|
|
692
690
|
return {
|
|
693
691
|
type: 'document',
|
|
694
|
-
source: { type: 'base64', media_type: 'application/pdf', data:
|
|
692
|
+
source: { type: 'base64', media_type: 'application/pdf', data: base64 },
|
|
695
693
|
};
|
|
696
694
|
}
|
|
697
695
|
if (part.mediaType === 'image/png' ||
|
|
@@ -700,7 +698,7 @@ function toAnthropicFileBlock(part) {
|
|
|
700
698
|
part.mediaType === 'image/webp') {
|
|
701
699
|
return {
|
|
702
700
|
type: 'image',
|
|
703
|
-
source: { type: 'base64', media_type: part.mediaType, data:
|
|
701
|
+
source: { type: 'base64', media_type: part.mediaType, data: base64 },
|
|
704
702
|
};
|
|
705
703
|
}
|
|
706
704
|
throw new Error(`Unsupported file media type "${part.mediaType}". Anthropic supports image/png, image/jpeg, image/gif, image/webp, application/pdf.`);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions';
|
|
2
|
-
import type {
|
|
2
|
+
import type { LanguageModelV4, LanguageModelV4CallOptions, LanguageModelV4FinishReason, LanguageModelV4Prompt } from '@ai-sdk/provider';
|
|
3
3
|
import type { LogBus } from '@salesforce/agentic-common';
|
|
4
4
|
import type { ModelConnectivityInfo, WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
|
|
5
|
-
import { type GatewayFetchWireHooks } from './
|
|
5
|
+
import { type GatewayFetchWireHooks } from './stream-mapping.js';
|
|
6
6
|
/**
|
|
7
|
-
* Builds the in-process `
|
|
7
|
+
* Builds the in-process `LanguageModelV4` impl that drives the `openai` SDK's
|
|
8
8
|
* `chat.completions.create({ stream: true })` against an OpenAI-compatible
|
|
9
9
|
* endpoint (LLM Gateway Express, LiteLLM, vLLM, etc. — anything that speaks
|
|
10
10
|
* `/v1/chat/completions`).
|
|
@@ -15,7 +15,7 @@ import { type GatewayFetchWireHooks } from './v3-stream-mapping.js';
|
|
|
15
15
|
* `input` split); (3) streaming events are `ChatCompletionChunk` objects with
|
|
16
16
|
* `choices[0].delta` text/tool-call deltas and the terminal `finish_reason`
|
|
17
17
|
* landing on the same chunk shape rather than a discrete `response.completed`
|
|
18
|
-
* event. The
|
|
18
|
+
* event. The V4 mapping output is identical — same `text-delta` / `tool-call` /
|
|
19
19
|
* `finish` parts the Responses builder produces.
|
|
20
20
|
*
|
|
21
21
|
* Per-call freshness contract is the same as the other builders: every HTTP
|
|
@@ -35,28 +35,28 @@ export declare function buildOpenAIChatCompletionsLanguageModel(opts: {
|
|
|
35
35
|
/** See `buildAnthropicLanguageModel` `getConsumerToolSchemas`. */
|
|
36
36
|
getConsumerToolSchemas?: () => ReadonlyMap<string, Record<string, unknown>>;
|
|
37
37
|
/** See `buildAnthropicLanguageModel` `getToolArgsRetryCounts`. */
|
|
38
|
-
getToolArgsRetryCounts?: (options:
|
|
38
|
+
getToolArgsRetryCounts?: (options: LanguageModelV4CallOptions) => Map<string, number>;
|
|
39
39
|
/** Per-read idle timeout (ms) for the gateway response body; see `createGatewayFetch`. */
|
|
40
40
|
idleReadTimeoutMs?: number;
|
|
41
|
-
}):
|
|
41
|
+
}): LanguageModelV4;
|
|
42
42
|
/**
|
|
43
|
-
* Translates a
|
|
43
|
+
* Translates a V4 `LanguageModelV4Prompt` into the OpenAI Chat Completions
|
|
44
44
|
* `messages` array shape. System messages stay as `system` role; user / tool
|
|
45
45
|
* follow naturally. Assistant text + tool-calls collapse onto a single
|
|
46
46
|
* assistant message carrying a `content` string (when text is present) plus
|
|
47
47
|
* `tool_calls` (when calls are present). Tool results land on `tool` role
|
|
48
48
|
* with `tool_call_id`.
|
|
49
49
|
*/
|
|
50
|
-
export declare function translatePromptToChatCompletions(prompt:
|
|
50
|
+
export declare function translatePromptToChatCompletions(prompt: LanguageModelV4Prompt): ChatCompletionMessageParam[];
|
|
51
51
|
/**
|
|
52
|
-
* Translates
|
|
52
|
+
* Translates V4 function-tool definitions into the Chat Completions `tools[]`
|
|
53
53
|
* shape. Skips non-function variants — same posture as the Responses builder.
|
|
54
54
|
*/
|
|
55
|
-
export declare function translateToolsToChatCompletions(options:
|
|
55
|
+
export declare function translateToolsToChatCompletions(options: LanguageModelV4CallOptions): ChatCompletionTool[];
|
|
56
56
|
/**
|
|
57
|
-
* Maps Chat Completions `finish_reason` onto
|
|
57
|
+
* Maps Chat Completions `finish_reason` onto V4's unified shape. The Chat
|
|
58
58
|
* Completions API uses `'stop' | 'length' | 'tool_calls' | 'content_filter' |
|
|
59
59
|
* 'function_call' | null`. `function_call` is the legacy shape from before
|
|
60
60
|
* `tool_calls` existed; modern endpoints emit `'tool_calls'`.
|
|
61
61
|
*/
|
|
62
|
-
export declare function mapChatCompletionFinishReason(raw: string | null, sawToolCalls: boolean):
|
|
62
|
+
export declare function mapChatCompletionFinishReason(raw: string | null, sawToolCalls: boolean): LanguageModelV4FinishReason;
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import OpenAI from 'openai';
|
|
6
6
|
import { EmptyStreamError } from './stream-errors.js';
|
|
7
|
-
import { SpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIUsage, toolResultOutputToString, } from './
|
|
7
|
+
import { SpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIUsage, toolResultOutputToString, v4FileDataToBase64, } from './stream-mapping.js';
|
|
8
8
|
/**
|
|
9
|
-
* Builds the in-process `
|
|
9
|
+
* Builds the in-process `LanguageModelV4` impl that drives the `openai` SDK's
|
|
10
10
|
* `chat.completions.create({ stream: true })` against an OpenAI-compatible
|
|
11
11
|
* endpoint (LLM Gateway Express, LiteLLM, vLLM, etc. — anything that speaks
|
|
12
12
|
* `/v1/chat/completions`).
|
|
@@ -17,7 +17,7 @@ import { SpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, cre
|
|
|
17
17
|
* `input` split); (3) streaming events are `ChatCompletionChunk` objects with
|
|
18
18
|
* `choices[0].delta` text/tool-call deltas and the terminal `finish_reason`
|
|
19
19
|
* landing on the same chunk shape rather than a discrete `response.completed`
|
|
20
|
-
* event. The
|
|
20
|
+
* event. The V4 mapping output is identical — same `text-delta` / `tool-call` /
|
|
21
21
|
* `finish` parts the Responses builder produces.
|
|
22
22
|
*
|
|
23
23
|
* Per-call freshness contract is the same as the other builders: every HTTP
|
|
@@ -41,7 +41,7 @@ export function buildOpenAIChatCompletionsLanguageModel(opts) {
|
|
|
41
41
|
idleReadTimeoutMs,
|
|
42
42
|
};
|
|
43
43
|
return {
|
|
44
|
-
specificationVersion: '
|
|
44
|
+
specificationVersion: 'v4',
|
|
45
45
|
provider: 'openai-compatible-chat-completions-pass-through',
|
|
46
46
|
modelId: initialInfo.nativeModelId,
|
|
47
47
|
supportedUrls: {},
|
|
@@ -131,7 +131,7 @@ async function doStream(ctx, options) {
|
|
|
131
131
|
// `stream_options.include_usage` opts the upstream into emitting a final
|
|
132
132
|
// chunk with cumulative usage on `usage`. Many OpenAI-compatible gateways
|
|
133
133
|
// honor this; those that don't simply omit the field, which we degrade
|
|
134
|
-
// to `undefined` `
|
|
134
|
+
// to `undefined` `LanguageModelV4Usage` per the missing-usage contract.
|
|
135
135
|
stream_options: { include_usage: true },
|
|
136
136
|
};
|
|
137
137
|
// See doGenerate: thread the AbortSignal into the SDK so a mid-stream abort
|
|
@@ -174,11 +174,11 @@ async function doStream(ctx, options) {
|
|
|
174
174
|
sawContentOrUsage = true;
|
|
175
175
|
}
|
|
176
176
|
if (delta.tool_calls) {
|
|
177
|
-
//
|
|
177
|
+
// V4 ordering: any open text span must close BEFORE tool-call parts
|
|
178
178
|
// land. Without this rotate, a `delta.content` chunk followed by a
|
|
179
179
|
// `delta.tool_calls` chunk in the same stream produces
|
|
180
180
|
// `text-start → text-delta → tool-call → text-end → finish`, which
|
|
181
|
-
// violates the
|
|
181
|
+
// violates the V4 lifecycle (peer builders pass-through-anthropic.ts
|
|
182
182
|
// and pass-through-openai.ts both rotate eagerly at the same moment).
|
|
183
183
|
// `rotate` is a no-op when no text span is open.
|
|
184
184
|
textSpan.rotate(controller);
|
|
@@ -297,7 +297,7 @@ async function doStream(ctx, options) {
|
|
|
297
297
|
if (terminalEmitted) {
|
|
298
298
|
// Symmetric defense with the Anthropic / OpenAI Responses builders —
|
|
299
299
|
// a trailing-JSON throw after the terminal `finish` is enqueued must
|
|
300
|
-
// not corrupt the
|
|
300
|
+
// not corrupt the V4 stream contract.
|
|
301
301
|
controller.close();
|
|
302
302
|
return;
|
|
303
303
|
}
|
|
@@ -309,7 +309,7 @@ async function doStream(ctx, options) {
|
|
|
309
309
|
return { stream };
|
|
310
310
|
}
|
|
311
311
|
/**
|
|
312
|
-
* Translates a
|
|
312
|
+
* Translates a V4 `LanguageModelV4Prompt` into the OpenAI Chat Completions
|
|
313
313
|
* `messages` array shape. System messages stay as `system` role; user / tool
|
|
314
314
|
* follow naturally. Assistant text + tool-calls collapse onto a single
|
|
315
315
|
* assistant message carrying a `content` string (when text is present) plus
|
|
@@ -378,7 +378,7 @@ export function translatePromptToChatCompletions(prompt) {
|
|
|
378
378
|
return messages;
|
|
379
379
|
}
|
|
380
380
|
/**
|
|
381
|
-
* Translates
|
|
381
|
+
* Translates V4 function-tool definitions into the Chat Completions `tools[]`
|
|
382
382
|
* shape. Skips non-function variants — same posture as the Responses builder.
|
|
383
383
|
*/
|
|
384
384
|
export function translateToolsToChatCompletions(options) {
|
|
@@ -396,16 +396,14 @@ export function translateToolsToChatCompletions(options) {
|
|
|
396
396
|
}));
|
|
397
397
|
}
|
|
398
398
|
function toChatCompletionsFilePart(part) {
|
|
399
|
-
|
|
400
|
-
throw new Error(`Unsupported file data for "${part.mediaType}": expected a base64 string, got ${part.data instanceof URL ? 'a URL' : 'binary data'}. Only base64-encoded file data is supported.`);
|
|
401
|
-
}
|
|
399
|
+
const base64 = v4FileDataToBase64(part.data, part.mediaType);
|
|
402
400
|
if (part.mediaType === 'image/png' ||
|
|
403
401
|
part.mediaType === 'image/jpeg' ||
|
|
404
402
|
part.mediaType === 'image/gif' ||
|
|
405
403
|
part.mediaType === 'image/webp') {
|
|
406
404
|
return {
|
|
407
405
|
type: 'image_url',
|
|
408
|
-
image_url: { url: `data:${part.mediaType};base64,${
|
|
406
|
+
image_url: { url: `data:${part.mediaType};base64,${base64}`, detail: 'auto' },
|
|
409
407
|
};
|
|
410
408
|
}
|
|
411
409
|
// Chat Completions doesn't accept inline file (PDF) parts the way Anthropic
|
|
@@ -414,7 +412,7 @@ function toChatCompletionsFilePart(part) {
|
|
|
414
412
|
// PDF support should use the Responses API path.
|
|
415
413
|
if (part.mediaType === 'application/pdf') {
|
|
416
414
|
const filename = part.filename ?? 'attachment.pdf';
|
|
417
|
-
const sizeKb = Math.round((
|
|
415
|
+
const sizeKb = Math.round((base64.length * 0.75) / 1024);
|
|
418
416
|
return {
|
|
419
417
|
type: 'text',
|
|
420
418
|
text: `[file: ${filename} (application/pdf, ~${sizeKb} KB) — file content not supported by /chat/completions; consider switching to providerHint: 'openai-responses' for PDF support]`,
|
|
@@ -424,7 +422,7 @@ function toChatCompletionsFilePart(part) {
|
|
|
424
422
|
`Supported: image/png, image/jpeg, image/gif, image/webp.`);
|
|
425
423
|
}
|
|
426
424
|
/**
|
|
427
|
-
* Maps Chat Completions usage onto the
|
|
425
|
+
* Maps Chat Completions usage onto the V4 `LanguageModelV4Usage` shape via the
|
|
428
426
|
* shared OpenAI usage mapper — Chat Completions and Responses surface the same
|
|
429
427
|
* post-normalisation shape (`prompt_tokens` / `completion_tokens` /
|
|
430
428
|
* `*_tokens_details`).
|
|
@@ -433,7 +431,7 @@ function mapChatCompletionUsage(usage) {
|
|
|
433
431
|
return mapOpenAIUsage(usage ?? undefined);
|
|
434
432
|
}
|
|
435
433
|
/**
|
|
436
|
-
* Maps Chat Completions `finish_reason` onto
|
|
434
|
+
* Maps Chat Completions `finish_reason` onto V4's unified shape. The Chat
|
|
437
435
|
* Completions API uses `'stop' | 'length' | 'tool_calls' | 'content_filter' |
|
|
438
436
|
* 'function_call' | null`. `function_call` is the legacy shape from before
|
|
439
437
|
* `tool_calls` existed; modern endpoints emit `'tool_calls'`.
|