@salesforce/sfdx-agent-chat-generations 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/LICENSE.txt +21 -0
- package/README.md +133 -0
- package/dist/chat-generations-language-model.d.ts +16 -0
- package/dist/chat-generations-language-model.js +402 -0
- package/dist/chat-generations-resolver.d.ts +72 -0
- package/dist/chat-generations-resolver.js +78 -0
- package/dist/chat-request.d.ts +117 -0
- package/dist/chat-request.js +14 -0
- package/dist/chat-response.d.ts +119 -0
- package/dist/chat-response.js +21 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +18 -0
- package/dist/gpt5-response-processor.d.ts +6 -0
- package/dist/gpt5-response-processor.js +189 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +52 -0
- package/dist/response-processor.d.ts +5 -0
- package/dist/response-processor.js +6 -0
- package/dist/sse-parser.d.ts +53 -0
- package/dist/sse-parser.js +116 -0
- package/dist/tool-arg-normalize.d.ts +1 -0
- package/dist/tool-arg-normalize.js +10 -0
- package/package.json +67 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@salesforce/sfdx-agent-chat-generations` are documented in this file.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [0.0.1] - 2026-07-27
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **chat-generations**: GovCloud chat/generations fallback @W-23560954@ ([#695](https://github.com/forcedotcom/agentic-dx/pull/695))
|
|
10
|
+
|
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Terms of Use
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Salesforce, Inc. All rights reserved.
|
|
4
|
+
|
|
5
|
+
These Terms of Use govern the download, installation, and/or use of this software provided by Salesforce, Inc. ("Salesforce") (the "Software"), were last updated on March 24, 2026, and constitute a legally binding agreement between you and Salesforce. If you do not agree to these Terms of Use, do not install or use the Software. The Software may link to third-party software components licensed under various open source licenses ("Open Source Components"). These Terms of Use pertain solely to Salesforce's proprietary code in the Software. It does not alter or extend any rights or obligations regarding the Open Source Components. For clarity, your use of the Open Source Components is governed by the terms of the applicable open source license(s). You are solely responsible for complying with the terms and conditions of those open source licenses.
|
|
6
|
+
|
|
7
|
+
Salesforce grants you a worldwide, non-exclusive, no-charge, royalty-free copyright license to reproduce, revocable, publicly display, publicly perform, sublicense, and distribute the Software and derivative works subject to these Terms. These Terms shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
Subject to the limited rights expressly granted hereunder, Salesforce reserves all rights, title, and interest in and to all intellectual property subsisting in the Software. No rights are granted to you hereunder other than as expressly set forth herein. Users residing in countries on the United States Office of Foreign Assets Control sanction list, or which are otherwise subject to a US export embargo, may not use the Software.
|
|
10
|
+
|
|
11
|
+
Implementation of the Software may require development work, for which you are responsible. The Software may contain bugs, errors and incompatibilities and is made available on an AS IS basis without support, updates, or service level commitments.
|
|
12
|
+
|
|
13
|
+
Salesforce reserves the right at any time to modify, suspend, or discontinue, the Software (or any part thereof) with or without notice. You agree that Salesforce shall not be liable to you or to any third party for any modification, suspension, or discontinuance.
|
|
14
|
+
|
|
15
|
+
You agree to defend Salesforce against any claim, demand, suit or proceeding made or brought against Salesforce by a third party arising out of or accruing from (a) your use of the Software, and (b) any application you develop with the Software that infringes any copyright, trademark, trade secret, trade dress, patent, or other intellectual property right of any person or defames any person or violates their rights of publicity or privacy (each a "Claim Against Salesforce"), and will indemnify Salesforce from any damages, attorney fees, and costs finally awarded against Salesforce as a result of, or for any amounts paid by Salesforce under a settlement approved by you in writing of, a Claim Against Salesforce, provided Salesforce (x) promptly gives you written notice of the Claim Against Salesforce, (y) gives you sole control of the defense and settlement of the Claim Against Salesforce (except that you may not settle any Claim Against Salesforce unless it unconditionally releases Salesforce of all liability), and (z) gives you all reasonable assistance, at your expense.
|
|
16
|
+
|
|
17
|
+
WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, THE SOFTWARE IS NOT SUPPORTED AND IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL SALESFORCE HAVE ANY LIABILITY FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, INDIRECT, SPECIAL, INCIDENTAL, PUNITIVE, OR CONSEQUENTIAL DAMAGES, OR DAMAGES BASED ON LOST PROFITS, DATA, OR USE, IN CONNECTION WITH THE SOFTWARE, HOWEVER CAUSED AND WHETHER IN CONTRACT, TORT, OR UNDER ANY OTHER THEORY OF LIABILITY, WHETHER OR NOT YOU HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
18
|
+
|
|
19
|
+
These Terms of Use shall be governed exclusively by the internal laws of the State of California, without regard to its conflicts of laws rules. Each party hereby consents to the exclusive jurisdiction of the state and federal courts located in San Francisco County, California to adjudicate any dispute arising out of or relating to these Terms of Use and the download, installation, and/or use of the Software. Except as expressly stated herein, these Terms of Use constitute the entire agreement between the parties, and supersede all prior and contemporaneous agreements, proposals, or representations, written or oral, concerning their subject matter. No modification, amendment, or waiver of any provision of these Terms of Use shall be effective unless it is by an update to these Terms of Use that Salesforce makes available, or is in writing and signed by the party against whom the modification, amendment, or waiver is to be asserted.
|
|
20
|
+
|
|
21
|
+
Data Privacy: Salesforce may collect, process, and store device, system, and other information related to your use of the Software. This information includes, but is not limited to, IP address, user metrics, and other data ("Usage Data"). Salesforce may use Usage Data for analytics, product development, and marketing purposes. You acknowledge that files generated in conjunction with the Software may contain sensitive or confidential data, and you are solely responsible for anonymizing and protecting such data.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @salesforce/sfdx-agent-chat-generations
|
|
2
|
+
|
|
3
|
+
Temporary GovCloud `chat/generations` fallback for
|
|
4
|
+
[`@salesforce/sfdx-agent-harness-mastra`](../sfdx-agent-harness-mastra). **This package is slated for removal** once the
|
|
5
|
+
Responses API is onboarded in GovCloud.
|
|
6
|
+
|
|
7
|
+
> **Closed source.** This package is published to npm under the [Salesforce Public Code License](../../LICENSE.txt) and
|
|
8
|
+
> is for use by Salesforce only.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
This package provides a temporary fallback for GovCloud deployments where the Responses API is not yet available. It
|
|
13
|
+
implements the legacy Salesforce LLM Gateway `chat/generations` wire shape — the only LLM path onboarded in GovCloud
|
|
14
|
+
(GIA2H) — and is injected into the Mastra harness via the generic `languageModelBuilders` seam and a
|
|
15
|
+
connectivity-resolver decorator. GPT-5 is the only supported model family. The fallback is expected to be deleted within
|
|
16
|
+
a few weeks once the Responses API is SFAP-onboarded in GovCloud (W-23560954).
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @salesforce/sfdx-agent-sdk @salesforce/sfdx-agent-harness-mastra @salesforce/sfdx-agent-chat-generations
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Bootstrap
|
|
27
|
+
|
|
28
|
+
Wire the GovCloud `chat/generations` fallback by injecting the resolver and language-model builder at construction time.
|
|
29
|
+
Two forms are supported:
|
|
30
|
+
|
|
31
|
+
#### Explicit construction
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
|
|
35
|
+
import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
|
|
36
|
+
import {
|
|
37
|
+
buildChatGenerationsLanguageModel,
|
|
38
|
+
ChatGenerationsResolver,
|
|
39
|
+
CHAT_GENERATIONS_PROVIDER_HINT,
|
|
40
|
+
} from '@salesforce/sfdx-agent-chat-generations';
|
|
41
|
+
|
|
42
|
+
const factory = new MastraHarnessFactory({
|
|
43
|
+
languageModelBuilders: {
|
|
44
|
+
[CHAT_GENERATIONS_PROVIDER_HINT]: buildChatGenerationsLanguageModel,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const connectivityResolver = new ChatGenerationsResolver({
|
|
49
|
+
delegate: myExistingResolver, // e.g., VibesAgentConnectivityResolver
|
|
50
|
+
baseUrl: 'https://dev.api.gov.salesforce.com/ai/gpt/v1',
|
|
51
|
+
extraHeaders: { 'x-salesforce-region': 'us-gov-east-1' }, // VERIFY: region header requirement TBD
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const manager = await createAgentManager(storageRoot, factory, { connectivityResolver });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Convenience factory (preferred)
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
|
|
61
|
+
import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
|
|
62
|
+
import { createChatGenerationsFallback } from '@salesforce/sfdx-agent-chat-generations';
|
|
63
|
+
|
|
64
|
+
const { connectivityResolver, languageModelBuilders } = createChatGenerationsFallback({
|
|
65
|
+
delegate: myExistingResolver,
|
|
66
|
+
baseUrl: 'https://dev.api.gov.salesforce.com/ai/gpt/v1',
|
|
67
|
+
extraHeaders: { 'x-salesforce-region': 'us-gov-east-1' }, // VERIFY: region header requirement TBD
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const factory = new MastraHarnessFactory({ languageModelBuilders });
|
|
71
|
+
const manager = await createAgentManager(storageRoot, factory, { connectivityResolver });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Important:** The consumer MUST set `AgentConfig.modelId` to a GovCloud-provisioned GPT model (e.g.,
|
|
75
|
+
`sfdc_ai__DefaultGPT4_8`). The SDK default (Claude Sonnet 4.6) will not resolve in GovCloud.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const agent = await manager.createAgent(projectRoot, {
|
|
79
|
+
modelId: 'sfdc_ai__DefaultGPT4_8', // Required: must be a GPT model provisioned in GovCloud
|
|
80
|
+
instructions: '...',
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The `x-salesforce-region` header requirement and value (`us-gov-east-1`) are unconfirmed pending live endpoint
|
|
85
|
+
validation. Consumers should verify these against the live GovCloud gateway.
|
|
86
|
+
|
|
87
|
+
## Public API
|
|
88
|
+
|
|
89
|
+
- **`buildChatGenerationsLanguageModel`** — `MastraLanguageModelBuilder` that produces a `LanguageModelV3` speaking the
|
|
90
|
+
`chat/generations` wire shape. Injects into
|
|
91
|
+
`MastraHarnessFactory.languageModelBuilders[CHAT_GENERATIONS_PROVIDER_HINT]`.
|
|
92
|
+
- **`ChatGenerationsResolver`** — Connectivity resolver decorator that wraps the consumer's real resolver and rewrites
|
|
93
|
+
its `ModelConnectivityInfo` bag onto the GovCloud `chat/generations` path (flips `baseUrl` + `providerHint`, merges
|
|
94
|
+
`extraHeaders`, reuses the delegate's `getHeaders()` verbatim).
|
|
95
|
+
- **`ChatGenerationsResolverOptions`** — Configuration type for `ChatGenerationsResolver`:
|
|
96
|
+
- `delegate: AgentConnectivityResolver` — the consumer's real resolver (e.g., `VibesAgentConnectivityResolver`).
|
|
97
|
+
- `baseUrl: string` — GovCloud gateway base URL (e.g., `'https://dev.api.gov.salesforce.com/ai/gpt/v1'`).
|
|
98
|
+
- `extraHeaders?: Record<string, string>` — optional extra headers required by GovCloud `chat/generations` (e.g.,
|
|
99
|
+
`{ 'x-salesforce-region': 'us-gov-east-1' }`). Unconfirmed.
|
|
100
|
+
- `enabled?: boolean` — kill-switch; when `false`, the resolver passes the delegate's result through unchanged.
|
|
101
|
+
Defaults to reading `process.env.SF_GOVCLOUD_LEGACY_FALLBACK === '1'`.
|
|
102
|
+
- **`CHAT_GENERATIONS_PROVIDER_HINT`** — Provider hint constant (`'salesforce-chat-generations'`) keying the injected
|
|
103
|
+
builder.
|
|
104
|
+
- **`createChatGenerationsFallback`** — Convenience factory returning `{ connectivityResolver, languageModelBuilders }`
|
|
105
|
+
ready to spread into `createAgentManager` and `MastraHarnessFactory` constructors.
|
|
106
|
+
- **`ChatGenerationsFallback`** — Return type of `createChatGenerationsFallback`
|
|
107
|
+
(`{ connectivityResolver: ChatGenerationsResolver, languageModelBuilders: Record<...> }`).
|
|
108
|
+
|
|
109
|
+
## Deletion
|
|
110
|
+
|
|
111
|
+
**This package is temporary.** When the Responses API is onboarded in GovCloud (expected within a few weeks):
|
|
112
|
+
|
|
113
|
+
1. Remove the consumer bootstrap wiring (stop passing `languageModelBuilders` to `MastraHarnessFactory` and stop
|
|
114
|
+
wrapping the resolver in `ChatGenerationsResolver`), or set `SF_GOVCLOUD_LEGACY_FALLBACK` unset / `enabled: false` to
|
|
115
|
+
disable at runtime.
|
|
116
|
+
2. Delete this package directory entirely.
|
|
117
|
+
3. Remove `sfdx-agent-chat-generations` from `.github/workflows/version-bump.yml` and the root `README.md` monorepo
|
|
118
|
+
index, then run `npm install` to drop it from the lockfile.
|
|
119
|
+
|
|
120
|
+
See [`DELETION.md`](DELETION.md) for the full deletion runbook.
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Build
|
|
126
|
+
npm run build -w @salesforce/sfdx-agent-chat-generations
|
|
127
|
+
|
|
128
|
+
# Test
|
|
129
|
+
npm run test -w @salesforce/sfdx-agent-chat-generations
|
|
130
|
+
|
|
131
|
+
# Lint
|
|
132
|
+
npm run lint -w @salesforce/sfdx-agent-chat-generations
|
|
133
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LanguageModelV3 } from '@ai-sdk/provider';
|
|
2
|
+
import type { MastraLanguageModelBuilderContext } from '@salesforce/sfdx-agent-harness-mastra';
|
|
3
|
+
/**
|
|
4
|
+
* Builds a bare `LanguageModelV3` that speaks the Salesforce LLM Gateway's legacy
|
|
5
|
+
* `chat/generations` wire shape (the only LLM path onboarded in GovCloud). The model
|
|
6
|
+
* POSTs `{ messages, generation_settings, model, system_prompt_strategy }` to
|
|
7
|
+
* `${baseUrl}/chat/generations{,/stream}`, reads the SSE / JSON `generation_details`
|
|
8
|
+
* envelope through {@link GPT5ResponseProcessor}, and maps the result to V3
|
|
9
|
+
* content / stream parts. GPT-5 is the only ported processor.
|
|
10
|
+
*
|
|
11
|
+
* The wire loop is inlined here (no gateway-client delegate): request-body assembly,
|
|
12
|
+
* the `/chat/generations{,/stream}` endpoint paths, and the SSE read loop are the
|
|
13
|
+
* pieces the deleted `RealLLMGatewayClient` owned; the V3 mapping is the piece the
|
|
14
|
+
* deleted `SalesforceMastraModelGateway` owned.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildChatGenerationsLanguageModel(ctx: MastraLanguageModelBuilderContext): LanguageModelV3;
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import { createParser } from 'eventsource-parser';
|
|
6
|
+
import { MimeType } from './chat-request.js';
|
|
7
|
+
import { GPT5ResponseProcessor } from './gpt5-response-processor.js';
|
|
8
|
+
import { createSSEEventHandler } from './sse-parser.js';
|
|
9
|
+
/**
|
|
10
|
+
* Builds a bare `LanguageModelV3` that speaks the Salesforce LLM Gateway's legacy
|
|
11
|
+
* `chat/generations` wire shape (the only LLM path onboarded in GovCloud). The model
|
|
12
|
+
* POSTs `{ messages, generation_settings, model, system_prompt_strategy }` to
|
|
13
|
+
* `${baseUrl}/chat/generations{,/stream}`, reads the SSE / JSON `generation_details`
|
|
14
|
+
* envelope through {@link GPT5ResponseProcessor}, and maps the result to V3
|
|
15
|
+
* content / stream parts. GPT-5 is the only ported processor.
|
|
16
|
+
*
|
|
17
|
+
* The wire loop is inlined here (no gateway-client delegate): request-body assembly,
|
|
18
|
+
* the `/chat/generations{,/stream}` endpoint paths, and the SSE read loop are the
|
|
19
|
+
* pieces the deleted `RealLLMGatewayClient` owned; the V3 mapping is the piece the
|
|
20
|
+
* deleted `SalesforceMastraModelGateway` owned.
|
|
21
|
+
*/
|
|
22
|
+
export function buildChatGenerationsLanguageModel(ctx) {
|
|
23
|
+
return {
|
|
24
|
+
specificationVersion: 'v3',
|
|
25
|
+
provider: 'salesforce-chat-generations',
|
|
26
|
+
get modelId() {
|
|
27
|
+
return ctx.getInfo().nativeModelId;
|
|
28
|
+
},
|
|
29
|
+
supportedUrls: {},
|
|
30
|
+
doGenerate: (options) => doGenerate(options, ctx),
|
|
31
|
+
doStream: (options) => doStream(options, ctx),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function doGenerate(options, ctx) {
|
|
35
|
+
const { getInfo, innerFetch } = ctx;
|
|
36
|
+
const info = getInfo();
|
|
37
|
+
const body = buildRequestBody(options, info.nativeModelId);
|
|
38
|
+
const response = await innerFetch(`${info.baseUrl}/chat/generations`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: await buildHeaders(info.getHeaders),
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
signal: options.abortSignal,
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw await gatewayError(response);
|
|
46
|
+
}
|
|
47
|
+
const raw = (await response.json());
|
|
48
|
+
const { generatedText, toolInvocations, usage: responseUsage, finishReason: rawFinishReason, } = new GPT5ResponseProcessor().processRawChatResponse(raw);
|
|
49
|
+
const content = [];
|
|
50
|
+
if (generatedText) {
|
|
51
|
+
content.push({ type: 'text', text: generatedText });
|
|
52
|
+
}
|
|
53
|
+
for (const tool of toolInvocations ?? []) {
|
|
54
|
+
content.push({
|
|
55
|
+
type: 'tool-call',
|
|
56
|
+
toolCallId: tool.id,
|
|
57
|
+
toolName: tool.function.name,
|
|
58
|
+
input: tool.function.arguments,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const sawToolCalls = (toolInvocations ?? []).length > 0;
|
|
62
|
+
return {
|
|
63
|
+
content,
|
|
64
|
+
finishReason: mapFinishReason(rawFinishReason, sawToolCalls),
|
|
65
|
+
usage: mapUsage(responseUsage),
|
|
66
|
+
warnings: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function doStream(options, ctx) {
|
|
70
|
+
const { getInfo, logBus, innerFetch } = ctx;
|
|
71
|
+
const info = getInfo();
|
|
72
|
+
const body = buildRequestBody(options, info.nativeModelId);
|
|
73
|
+
const response = await innerFetch(`${info.baseUrl}/chat/generations/stream`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: await buildHeaders(info.getHeaders),
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
signal: options.abortSignal,
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw await gatewayError(response);
|
|
81
|
+
}
|
|
82
|
+
// Stage 1 — read the response body (a ReadableStream<Uint8Array>) through
|
|
83
|
+
// eventsource-parser into RawChatChunks. Faithfully mirrors the deleted
|
|
84
|
+
// RealLLMGatewayClient.requestStream: feed each decoded chunk to the parser and
|
|
85
|
+
// drain the shared queue as an async generator.
|
|
86
|
+
const responseBody = response.body;
|
|
87
|
+
const modelId = info.nativeModelId;
|
|
88
|
+
async function* rawChunks() {
|
|
89
|
+
if (!responseBody) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const decoder = new TextDecoder();
|
|
93
|
+
const queue = [];
|
|
94
|
+
const parser = createParser({
|
|
95
|
+
onEvent: createSSEEventHandler(queue, undefined, ({ chunk, reason }) => {
|
|
96
|
+
logBus?.warn('SSE parse error', { modelId, chunk, reason });
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
for await (const bytes of responseBody) {
|
|
100
|
+
parser.feed(decoder.decode(bytes, { stream: true }));
|
|
101
|
+
while (queue.length > 0) {
|
|
102
|
+
yield queue.shift();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Stage 2 — the GPT-5 processor turns RawChatChunks into normalized ChatStreamChunks
|
|
107
|
+
// (tool-call accumulation + the trailing usage-only chunk).
|
|
108
|
+
const processed = new GPT5ResponseProcessor().processRawChatStream(rawChunks());
|
|
109
|
+
// Stage 3 — map each ChatStreamChunk onto the V3 text-start / text-delta / text-end /
|
|
110
|
+
// tool-call / finish lifecycle (lifted verbatim from the deleted bridge's doStream).
|
|
111
|
+
let sawToolCalls = false;
|
|
112
|
+
let textSpanOpen = false;
|
|
113
|
+
let textSpanId = 0;
|
|
114
|
+
const stream = new ReadableStream({
|
|
115
|
+
start: async (controller) => {
|
|
116
|
+
try {
|
|
117
|
+
// V3 requires a `usage` field on `finish`; leave undefined here so we can
|
|
118
|
+
// detect a stream that ended without a usage chunk (W-22692131).
|
|
119
|
+
let lastUsage;
|
|
120
|
+
for await (const chunk of processed) {
|
|
121
|
+
if (chunk.generatedText && !chunk.done) {
|
|
122
|
+
if (!textSpanOpen) {
|
|
123
|
+
controller.enqueue({ type: 'text-start', id: `text-${textSpanId}` });
|
|
124
|
+
textSpanOpen = true;
|
|
125
|
+
}
|
|
126
|
+
controller.enqueue({
|
|
127
|
+
type: 'text-delta',
|
|
128
|
+
delta: chunk.generatedText,
|
|
129
|
+
id: `text-${textSpanId}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (chunk.toolInvocations?.length) {
|
|
133
|
+
// Close any open text span before emitting tool calls so text after
|
|
134
|
+
// tools gets a new span id (AI SDK v5 lifecycle).
|
|
135
|
+
if (textSpanOpen) {
|
|
136
|
+
controller.enqueue({ type: 'text-end', id: `text-${textSpanId}` });
|
|
137
|
+
textSpanOpen = false;
|
|
138
|
+
textSpanId++;
|
|
139
|
+
}
|
|
140
|
+
sawToolCalls = true;
|
|
141
|
+
for (const tool of chunk.toolInvocations) {
|
|
142
|
+
controller.enqueue({
|
|
143
|
+
type: 'tool-call',
|
|
144
|
+
toolCallId: tool.id,
|
|
145
|
+
toolName: tool.function.name,
|
|
146
|
+
input: tool.function.arguments,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (chunk.usage) {
|
|
151
|
+
lastUsage = mapUsage(chunk.usage);
|
|
152
|
+
}
|
|
153
|
+
if (chunk.done) {
|
|
154
|
+
if (textSpanOpen) {
|
|
155
|
+
controller.enqueue({ type: 'text-end', id: `text-${textSpanId}` });
|
|
156
|
+
}
|
|
157
|
+
const finishReason = mapFinishReason(chunk.finishReason, sawToolCalls);
|
|
158
|
+
if (lastUsage === undefined) {
|
|
159
|
+
logBus?.warn('chat/generations stream ended without a usage chunk', {
|
|
160
|
+
modelId,
|
|
161
|
+
finishReason: chunk.finishReason,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
controller.enqueue({
|
|
165
|
+
type: 'finish',
|
|
166
|
+
finishReason,
|
|
167
|
+
usage: lastUsage ?? mapUsage(undefined),
|
|
168
|
+
});
|
|
169
|
+
controller.close();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
controller.close();
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
// Mid-stream failures are yielded, not thrown (harness contract + deleted bridge).
|
|
177
|
+
controller.enqueue({ type: 'error', error });
|
|
178
|
+
controller.close();
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
return { stream };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Assembles the outbound request body: the translated `ChatRequest` plus the
|
|
186
|
+
* `model` + `system_prompt_strategy` augmentation the gateway requires (from the
|
|
187
|
+
* deleted client's `chat` / `chatStream`).
|
|
188
|
+
*/
|
|
189
|
+
function buildRequestBody(options, nativeModelId) {
|
|
190
|
+
return {
|
|
191
|
+
...buildChatRequest(options),
|
|
192
|
+
model: nativeModelId,
|
|
193
|
+
system_prompt_strategy: 'use_model_parameter',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function buildChatRequest(options) {
|
|
197
|
+
const messages = translatePrompt(options.prompt);
|
|
198
|
+
const tools = translateTools(options);
|
|
199
|
+
const chatRequest = {
|
|
200
|
+
messages,
|
|
201
|
+
generation_settings: {
|
|
202
|
+
max_tokens: options.maxOutputTokens,
|
|
203
|
+
temperature: options.temperature,
|
|
204
|
+
stop_sequences: options.stopSequences,
|
|
205
|
+
frequency_penalty: options.frequencyPenalty,
|
|
206
|
+
presence_penalty: options.presencePenalty,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
if (tools.length > 0) {
|
|
210
|
+
chatRequest.tools = tools;
|
|
211
|
+
}
|
|
212
|
+
return chatRequest;
|
|
213
|
+
}
|
|
214
|
+
function translatePrompt(prompt) {
|
|
215
|
+
return prompt.flatMap((message) => {
|
|
216
|
+
switch (message.role) {
|
|
217
|
+
case 'system':
|
|
218
|
+
return { role: 'system', content: message.content };
|
|
219
|
+
case 'user': {
|
|
220
|
+
const content = message.content
|
|
221
|
+
.filter((p) => p.type === 'text')
|
|
222
|
+
.map((p) => p.text)
|
|
223
|
+
.join('');
|
|
224
|
+
const files = message.content
|
|
225
|
+
.filter((p) => p.type === 'file')
|
|
226
|
+
.map((p) => toChatMessageFile(p));
|
|
227
|
+
const msg = { role: 'user', content };
|
|
228
|
+
if (files.length > 0) {
|
|
229
|
+
msg.files = files;
|
|
230
|
+
}
|
|
231
|
+
return msg;
|
|
232
|
+
}
|
|
233
|
+
case 'assistant': {
|
|
234
|
+
const textParts = message.content
|
|
235
|
+
.filter((p) => p.type === 'text')
|
|
236
|
+
.map((p) => p.text)
|
|
237
|
+
.join('');
|
|
238
|
+
const toolInvocations = message.content
|
|
239
|
+
.filter((p) => p.type === 'tool-call')
|
|
240
|
+
.map((tc) => ({
|
|
241
|
+
id: tc.toolCallId,
|
|
242
|
+
function: {
|
|
243
|
+
name: tc.toolName,
|
|
244
|
+
arguments: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input ?? {}),
|
|
245
|
+
},
|
|
246
|
+
}));
|
|
247
|
+
const msg = { role: 'assistant', content: textParts };
|
|
248
|
+
if (toolInvocations.length > 0) {
|
|
249
|
+
msg.tool_invocations = toolInvocations;
|
|
250
|
+
}
|
|
251
|
+
return msg;
|
|
252
|
+
}
|
|
253
|
+
case 'tool':
|
|
254
|
+
// V3's `tool` role content can carry `tool-approval-response` parts alongside
|
|
255
|
+
// `tool-result` parts. We never surface tool-approval-request to consumers, so
|
|
256
|
+
// approval-response parts don't arrive in practice — filter defensively so the
|
|
257
|
+
// wire stays forward-compatible with future approval flows.
|
|
258
|
+
return message.content
|
|
259
|
+
.filter((part) => part.type === 'tool-result')
|
|
260
|
+
.map((part) => ({
|
|
261
|
+
role: 'tool',
|
|
262
|
+
content: toolResultOutputToString(part.output),
|
|
263
|
+
tool_call_id: part.toolCallId,
|
|
264
|
+
tool_call_name: part.toolName,
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Translates an AI SDK V3 user file part into the gateway's {@link ChatMessageFile}.
|
|
271
|
+
* v1 only supports base64-string `data`; non-string data (`Uint8Array` / `URL`) and
|
|
272
|
+
* MIME types outside the gateway allowlist are rejected — both indicate a caller that
|
|
273
|
+
* bypassed the harness input mapping (which validates caps pre-stream).
|
|
274
|
+
*/
|
|
275
|
+
function toChatMessageFile(part) {
|
|
276
|
+
if (typeof part.data !== 'string') {
|
|
277
|
+
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.`);
|
|
278
|
+
}
|
|
279
|
+
if (!isSupportedMimeType(part.mediaType)) {
|
|
280
|
+
throw new Error(`Unsupported file media type "${part.mediaType}". Supported types: ${Object.values(MimeType).join(', ')}.`);
|
|
281
|
+
}
|
|
282
|
+
const file = {
|
|
283
|
+
// The gateway wants a per-file handle; it is wire-opaque, so a direct Web Crypto
|
|
284
|
+
// UUID is sufficient here (W-23560954: temporary fallback, id is a wire-opaque handle).
|
|
285
|
+
fileId: globalThis.crypto.randomUUID(),
|
|
286
|
+
mimeType: part.mediaType,
|
|
287
|
+
dataType: 'base64',
|
|
288
|
+
data: part.data,
|
|
289
|
+
};
|
|
290
|
+
if (part.filename !== undefined) {
|
|
291
|
+
file.fileName = part.filename;
|
|
292
|
+
}
|
|
293
|
+
return file;
|
|
294
|
+
}
|
|
295
|
+
function toolResultOutputToString(output) {
|
|
296
|
+
switch (output.type) {
|
|
297
|
+
case 'text':
|
|
298
|
+
case 'error-text':
|
|
299
|
+
return output.value;
|
|
300
|
+
case 'json':
|
|
301
|
+
case 'error-json':
|
|
302
|
+
return JSON.stringify(output.value);
|
|
303
|
+
case 'content':
|
|
304
|
+
return output.value
|
|
305
|
+
.filter((v) => v.type === 'text')
|
|
306
|
+
.map((v) => v.text)
|
|
307
|
+
.join('');
|
|
308
|
+
case 'execution-denied':
|
|
309
|
+
return output.reason ?? 'tool execution denied';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function translateTools(options) {
|
|
313
|
+
if (!options.tools)
|
|
314
|
+
return [];
|
|
315
|
+
return options.tools
|
|
316
|
+
.filter((t) => t.type === 'function')
|
|
317
|
+
.map((tool) => ({
|
|
318
|
+
type: 'function',
|
|
319
|
+
function: {
|
|
320
|
+
name: tool.name,
|
|
321
|
+
description: tool.description,
|
|
322
|
+
parameters: tool.inputSchema,
|
|
323
|
+
},
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
/** Narrows an arbitrary media-type string to the gateway's {@link MimeType} allowlist. */
|
|
327
|
+
function isSupportedMimeType(mediaType) {
|
|
328
|
+
return Object.values(MimeType).includes(mediaType);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Builds the outbound header set. The resolver's `getHeaders()` already carries the
|
|
332
|
+
* full set (Authorization, tenant, feature id, …); we only default `Content-Type` and
|
|
333
|
+
* let a resolver-supplied `Content-Type` win. Called per request so JWT rotation lands
|
|
334
|
+
* on the next call (never cached at builder construction).
|
|
335
|
+
*/
|
|
336
|
+
async function buildHeaders(getHeaders) {
|
|
337
|
+
const resolved = await getHeaders();
|
|
338
|
+
return { 'Content-Type': 'application/json;charset=utf-8', ...resolved };
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Reads a non-2xx response and produces a clear error carrying the status and a
|
|
342
|
+
* bounded snippet of the body. Pre-stream failure: thrown from `doGenerate` /
|
|
343
|
+
* rejected from `doStream` (matches the deleted client, which threw on non-2xx before
|
|
344
|
+
* any stream was returned).
|
|
345
|
+
*/
|
|
346
|
+
async function gatewayError(response) {
|
|
347
|
+
let snippet;
|
|
348
|
+
try {
|
|
349
|
+
snippet = (await response.text()).slice(0, 500);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
snippet = '<unreadable body>';
|
|
353
|
+
}
|
|
354
|
+
return new Error(`chat/generations request failed with status ${response.status}: ${snippet}`);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Maps a gateway raw finish-reason string to the V3 `LanguageModelV3FinishReason`
|
|
358
|
+
* (a `{ unified, raw }` object). When `sawToolCalls` is true, `unified` is forced to
|
|
359
|
+
* `'tool-calls'` regardless of what the gateway returned — the gateway can report a
|
|
360
|
+
* non-tool reason even with tool invocations present, and Mastra's downstream loop
|
|
361
|
+
* relies on `'tool-calls'` to schedule the next step. Handles both GPT
|
|
362
|
+
* (`stop` / `length` / `tool_calls`) and legacy Claude (`max_tokens` / `tool_use`)
|
|
363
|
+
* vocabularies for robustness.
|
|
364
|
+
*/
|
|
365
|
+
function mapFinishReason(rawFinishReason, sawToolCalls) {
|
|
366
|
+
const unified = sawToolCalls
|
|
367
|
+
? 'tool-calls'
|
|
368
|
+
: rawFinishReason === 'length' || rawFinishReason === 'max_tokens'
|
|
369
|
+
? 'length'
|
|
370
|
+
: rawFinishReason === 'tool_calls' || rawFinishReason === 'tool_use'
|
|
371
|
+
? 'tool-calls'
|
|
372
|
+
: 'stop';
|
|
373
|
+
return { unified, raw: rawFinishReason };
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Maps the gateway's flat `TokenUsage` to the V3 nested `LanguageModelV3Usage`.
|
|
377
|
+
* Reasoning tokens roll up into `outputTokens.total` (V3 spec:
|
|
378
|
+
* `outputTokens.total = text + reasoning`) so GPT-5 reasoning tokens survive Mastra's
|
|
379
|
+
* recomputed flat `totalTokens`. Cache tokens populate `inputTokens.cacheRead` /
|
|
380
|
+
* `cacheWrite` without rolling into `inputTokens.total`.
|
|
381
|
+
*/
|
|
382
|
+
function mapUsage(responseUsage) {
|
|
383
|
+
const outputTokens = responseUsage?.outputTokens;
|
|
384
|
+
const reasoningTokens = responseUsage?.reasoningTokens;
|
|
385
|
+
const outputTotal = outputTokens === undefined && reasoningTokens === undefined
|
|
386
|
+
? undefined
|
|
387
|
+
: (outputTokens ?? 0) + (reasoningTokens ?? 0);
|
|
388
|
+
return {
|
|
389
|
+
inputTokens: {
|
|
390
|
+
total: responseUsage?.inputTokens,
|
|
391
|
+
noCache: undefined,
|
|
392
|
+
cacheRead: responseUsage?.cacheReadTokens,
|
|
393
|
+
cacheWrite: responseUsage?.cacheWriteTokens,
|
|
394
|
+
},
|
|
395
|
+
outputTokens: {
|
|
396
|
+
total: outputTotal,
|
|
397
|
+
text: outputTokens,
|
|
398
|
+
reasoning: reasoningTokens,
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
//# sourceMappingURL=chat-generations-language-model.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { AgentConfig, AgentConnectivityResolver, ResolvedConnectivity } from '@salesforce/sfdx-agent-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Provider hint for the GovCloud `chat/generations` wire shape. Used to dispatch
|
|
4
|
+
* to the injected builder in the Mastra harness factory.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CHAT_GENERATIONS_PROVIDER_HINT = "salesforce-chat-generations";
|
|
7
|
+
/**
|
|
8
|
+
* Configuration options for the GovCloud `chat/generations` resolver decorator.
|
|
9
|
+
*/
|
|
10
|
+
export interface ChatGenerationsResolverOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The consumer's real connectivity resolver (e.g., AFV's VibesAgentConnectivityResolver
|
|
13
|
+
* or DefaultAgentConnectivityResolver). The decorator wraps this delegate and rewrites
|
|
14
|
+
* its connectivity bag onto the GovCloud `chat/generations` path.
|
|
15
|
+
*/
|
|
16
|
+
delegate: AgentConnectivityResolver;
|
|
17
|
+
/**
|
|
18
|
+
* GovCloud gateway base URL, e.g., `'https://dev.api.gov.salesforce.com/ai/gpt/v1'`.
|
|
19
|
+
* This replaces the delegate's `baseUrl` when the resolver is enabled.
|
|
20
|
+
*/
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
/**
|
|
23
|
+
* Optional extra headers required by GovCloud `chat/generations`
|
|
24
|
+
* (e.g., `{ 'x-salesforce-region': 'us-gov-east-1' }`). These are merged
|
|
25
|
+
* on top of the delegate's headers, with extras winning on key conflict.
|
|
26
|
+
* Verify with live GovCloud endpoint whether any headers beyond standard
|
|
27
|
+
* gateway headers are required (W-23560954).
|
|
28
|
+
*/
|
|
29
|
+
extraHeaders?: Record<string, string>;
|
|
30
|
+
/**
|
|
31
|
+
* Kill-switch: when `false`, the resolver passes the delegate's result through
|
|
32
|
+
* unchanged (zero behavior change). When unset, defaults to reading
|
|
33
|
+
* `process.env.SF_GOVCLOUD_LEGACY_FALLBACK === '1'`.
|
|
34
|
+
*/
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Decorator over a consumer's real `AgentConnectivityResolver` that flips an existing
|
|
39
|
+
* connectivity bag onto the GovCloud `chat/generations` path. Reuses the delegate's
|
|
40
|
+
* `getHeaders()` verbatim (JWT rotation + tenant/feature headers for free), merging
|
|
41
|
+
* any GovCloud-required extras on top.
|
|
42
|
+
*
|
|
43
|
+
* **Why a decorator, not a subclass:** `DefaultAgentConnectivityResolver` and
|
|
44
|
+
* `buildSalesforceGatewayHeaders` are not on the SDK's public surface — subclassing
|
|
45
|
+
* would force new public exports. This decorator wraps the consumer's existing
|
|
46
|
+
* resolver (e.g., AFV's `VibesAgentConnectivityResolver`) and reuses its `getHeaders()`
|
|
47
|
+
* without needing SDK-internal header helpers.
|
|
48
|
+
*
|
|
49
|
+
* **Gating is explicit, not auto-detected.** Do not sniff `instanceUrl` for `.gov` —
|
|
50
|
+
* domain research showed that's unreliable (org instanceUrl differs from gateway host,
|
|
51
|
+
* and civilian gov is `*.salesforce.com`, indistinguishable from commercial). The
|
|
52
|
+
* consumer knows they're deploying to GovCloud; they wire this decorator at construction.
|
|
53
|
+
*
|
|
54
|
+
* **Consumer must set a GPT `modelId`.** The SDK default (Claude Sonnet 4.6) will not
|
|
55
|
+
* resolve in GovCloud. AFV must bind `AgentConfig.modelId` to a GovCloud-provisioned
|
|
56
|
+
* `sfdc_ai__DefaultGPT*` model id.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const resolver = new ChatGenerationsResolver({
|
|
61
|
+
* delegate: myExistingResolver,
|
|
62
|
+
* baseUrl: 'https://dev.api.gov.salesforce.com/ai/gpt/v1',
|
|
63
|
+
* extraHeaders: { 'x-salesforce-region': 'us-gov-east-1' },
|
|
64
|
+
* });
|
|
65
|
+
* const manager = await createAgentManager(storageRoot, factory, { connectivityResolver: resolver });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare class ChatGenerationsResolver implements AgentConnectivityResolver {
|
|
69
|
+
private readonly opts;
|
|
70
|
+
constructor(opts: ChatGenerationsResolverOptions);
|
|
71
|
+
resolve(projectRoot: string, config: AgentConfig): Promise<ResolvedConnectivity>;
|
|
72
|
+
}
|