@superjackfruitlabs/salaam 0.0.1 → 0.0.2
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 +15 -0
- package/README.md +25 -17
- package/dist/src/a2a-backend.js +225 -0
- package/dist/src/a2a-inbound.js +487 -0
- package/dist/src/a2a-peer.js +370 -0
- package/dist/src/a2a-protocol.js +250 -0
- package/dist/src/agent-profiles.js +289 -0
- package/dist/src/children.js +132 -26
- package/dist/src/claude-provider.js +2 -179
- package/dist/src/cli-help.js +107 -6
- package/dist/src/cli.js +477 -13
- package/dist/src/config.js +26 -3
- package/dist/src/credentials.js +7 -2
- package/dist/src/external-runtime.js +1 -0
- package/dist/src/external-work.js +583 -0
- package/dist/src/harness-backend.js +466 -0
- package/dist/src/harness-container.js +450 -0
- package/dist/src/harness-model-gateway.js +104 -0
- package/dist/src/harness-prepare.js +88 -0
- package/dist/src/harness-probe.js +111 -0
- package/dist/src/harness-protocol.js +325 -0
- package/dist/src/harness-readiness.js +50 -0
- package/dist/src/harness-rpc.js +206 -0
- package/dist/src/harness-tool-identity.js +87 -0
- package/dist/src/harness-wire-bridge.js +387 -0
- package/dist/src/host.js +6 -1
- package/dist/src/house-extension.js +147 -64
- package/dist/src/live-provider.js +301 -0
- package/dist/src/managed.js +33 -3
- package/dist/src/protocol.js +9 -5
- package/dist/src/provider-auth.js +300 -0
- package/dist/src/provider-config.js +65 -17
- package/dist/src/provider-model.js +78 -0
- package/dist/src/provider-payload.js +264 -0
- package/dist/src/provider-transport.js +36 -0
- package/dist/src/provider-usage.js +59 -0
- package/dist/src/repository-snapshots.js +219 -0
- package/dist/src/service.js +274 -9
- package/dist/src/store.js +44 -2
- package/dist/src/supervisor.js +246 -35
- package/dist/src/task-budget.js +17 -8
- package/dist/src/task-context.js +15 -0
- package/dist/src/task-driver.js +38 -14
- package/dist/src/task-policy.js +20 -1
- package/dist/src/task-verification.js +3 -0
- package/dist/src/task-work.js +31 -0
- package/dist/src/team-work.js +378 -0
- package/dist/src/tui-view.js +65 -10
- package/dist/src/tui.js +4 -2
- package/dist/src/usage.js +18 -0
- package/dist/src/worker-boundary.js +298 -0
- package/dist/src/worker-workspaces.js +274 -0
- package/dist/src/workspaces.js +6 -2
- package/harness-runtime/codex-pins.json +10 -0
- package/harness-runtime/package-lock.json +560 -0
- package/harness-runtime/package.json +9 -0
- package/npm-shrinkwrap.json +57 -2
- package/package.json +2 -1
- package/src/a2a-backend.ts +254 -0
- package/src/a2a-inbound.ts +585 -0
- package/src/a2a-peer.ts +441 -0
- package/src/a2a-protocol.ts +286 -0
- package/src/agent-profiles.ts +379 -0
- package/src/children.ts +168 -27
- package/src/claude-provider.ts +2 -189
- package/src/cli-help.ts +107 -6
- package/src/cli.ts +602 -12
- package/src/config.ts +34 -3
- package/src/credentials.ts +9 -2
- package/src/external-runtime.ts +54 -0
- package/src/external-work.ts +663 -0
- package/src/harness-backend.ts +557 -0
- package/src/harness-container.ts +543 -0
- package/src/harness-model-gateway.ts +138 -0
- package/src/harness-prepare.ts +113 -0
- package/src/harness-probe.ts +150 -0
- package/src/harness-protocol.ts +388 -0
- package/src/harness-readiness.ts +71 -0
- package/src/harness-rpc.ts +244 -0
- package/src/harness-tool-identity.ts +91 -0
- package/src/harness-wire-bridge.ts +418 -0
- package/src/host.ts +11 -6
- package/src/house-extension.ts +165 -66
- package/src/live-provider.ts +347 -0
- package/src/managed.ts +43 -4
- package/src/protocol.ts +36 -8
- package/src/provider-auth.ts +336 -0
- package/src/provider-config.ts +89 -18
- package/src/provider-model.ts +86 -0
- package/src/provider-payload.ts +263 -0
- package/src/provider-transport.ts +51 -0
- package/src/provider-usage.ts +64 -0
- package/src/repository-snapshots.ts +259 -0
- package/src/service.ts +446 -11
- package/src/store.ts +60 -4
- package/src/supervisor.ts +294 -41
- package/src/task-budget.ts +24 -11
- package/src/task-context.ts +17 -0
- package/src/task-driver.ts +51 -15
- package/src/task-policy.ts +23 -3
- package/src/task-verification.ts +2 -0
- package/src/task-work.ts +46 -1
- package/src/team-work.ts +554 -0
- package/src/tui-client.ts +5 -0
- package/src/tui-view.ts +164 -12
- package/src/tui.ts +5 -2
- package/src/usage.ts +17 -0
- package/src/worker-boundary.ts +395 -0
- package/src/worker-workspaces.ts +390 -0
- package/src/workspaces.ts +4 -2
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { clampMaxTokensToContext } from '@earendil-works/pi-ai/api/simple-options';
|
|
2
|
+
import type { Api, Model, AuthResult, TranscriptContext } from '@earendil-works/pi-ai';
|
|
3
|
+
import { canonical } from './protocol.js';
|
|
4
|
+
import { getProviderModel, ROUTING_FIELDS, type ProviderRoute } from './provider-config.js';
|
|
5
|
+
export function materialize(model: Model<Api>, auth: AuthResult): Model<Api> {
|
|
6
|
+
let baseUrl = auth.auth.baseUrl ?? model.baseUrl;
|
|
7
|
+
for (const [key, value] of Object.entries(auth.env ?? {}))
|
|
8
|
+
baseUrl = baseUrl.replaceAll('{' + key + '}', value);
|
|
9
|
+
if (model.provider === 'azure') {
|
|
10
|
+
const env = auth.env ?? {};
|
|
11
|
+
baseUrl =
|
|
12
|
+
env.AZURE_OPENAI_BASE_URL ??
|
|
13
|
+
(env.AZURE_OPENAI_RESOURCE_NAME
|
|
14
|
+
? `https://${env.AZURE_OPENAI_RESOURCE_NAME}.openai.azure.com/openai/v1`
|
|
15
|
+
: baseUrl);
|
|
16
|
+
}
|
|
17
|
+
if (model.provider === 'google-vertex') {
|
|
18
|
+
const location = auth.env?.GOOGLE_CLOUD_LOCATION ?? 'global';
|
|
19
|
+
if (!/^[a-z0-9-]+$/.test(location)) throw new Error('Invalid Google Cloud location');
|
|
20
|
+
baseUrl =
|
|
21
|
+
location === 'global'
|
|
22
|
+
? 'https://aiplatform.googleapis.com'
|
|
23
|
+
: `https://${location}-aiplatform.googleapis.com`;
|
|
24
|
+
}
|
|
25
|
+
if (model.provider === 'amazon-bedrock') {
|
|
26
|
+
const region =
|
|
27
|
+
/^arn:aws[^:]*:bedrock:([^:]+):/.exec(model.id)?.[1] ??
|
|
28
|
+
auth.env?.AWS_REGION ??
|
|
29
|
+
auth.env?.AWS_DEFAULT_REGION;
|
|
30
|
+
if (region) {
|
|
31
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(region)) throw new Error('Invalid AWS region');
|
|
32
|
+
baseUrl = `https://bedrock-runtime.${region}.amazonaws.com${region.startsWith('cn-') ? '.cn' : ''}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const url = new URL(baseUrl);
|
|
36
|
+
if (
|
|
37
|
+
url.protocol !== 'https:' ||
|
|
38
|
+
url.username ||
|
|
39
|
+
url.password ||
|
|
40
|
+
url.hash ||
|
|
41
|
+
url.search ||
|
|
42
|
+
/[{}]/.test(baseUrl)
|
|
43
|
+
)
|
|
44
|
+
throw new Error('Provider endpoint is not configured');
|
|
45
|
+
return {
|
|
46
|
+
...model,
|
|
47
|
+
baseUrl: baseUrl.replace(/\/$/, ''),
|
|
48
|
+
headers: model.headers,
|
|
49
|
+
compat: {
|
|
50
|
+
...model.compat,
|
|
51
|
+
supportsMidConvoSystemMessages: false,
|
|
52
|
+
supportsMidConvoToolChanges: false,
|
|
53
|
+
supportsMidConvoEffort: false,
|
|
54
|
+
supportsAdditionalTools: false,
|
|
55
|
+
supportsToolSearch: false,
|
|
56
|
+
supportsOpenAIGrammarTools: false,
|
|
57
|
+
allowedFallbackModels: [],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Non-secret routing configuration becomes part of durable session identity. */
|
|
63
|
+
export function bindProviderConfiguration(route: ProviderRoute, auth: AuthResult): ProviderRoute {
|
|
64
|
+
const catalog = getProviderModel(route);
|
|
65
|
+
if (!catalog) throw new Error('Pinned provider model missing');
|
|
66
|
+
const model = materialize(catalog, auth);
|
|
67
|
+
const configuration = Object.fromEntries(
|
|
68
|
+
ROUTING_FIELDS.flatMap((key) => (auth.env?.[key] ? [[key, auth.env[key]!]] : [])),
|
|
69
|
+
);
|
|
70
|
+
const changed =
|
|
71
|
+
route.provider !== 'anthropic' ||
|
|
72
|
+
model.baseUrl !== catalog.baseUrl ||
|
|
73
|
+
Object.keys(configuration).length > 0;
|
|
74
|
+
const binding = changed ? { endpoint: model.baseUrl, configuration } : undefined;
|
|
75
|
+
if (route.connection && canonical(route.connection) !== canonical(binding))
|
|
76
|
+
throw new Error(
|
|
77
|
+
'Provider endpoint or routing configuration changed; use the original configuration or a new session',
|
|
78
|
+
);
|
|
79
|
+
return binding ? { ...route, connection: binding } : route;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Leave room for the answer instead of emitting Pi's minimum 1,024-token thinking budget under a smaller ceiling. */
|
|
83
|
+
export function generationOptions(model: Model<Api>, context: TranscriptContext, cap: number) {
|
|
84
|
+
const maxTokens = clampMaxTokensToContext(model, context, cap);
|
|
85
|
+
return { maxTokens, reasoning: maxTokens >= 2048 ? ('low' as const) : undefined };
|
|
86
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { getCurrentTools, type TranscriptContext } from '@earendil-works/pi-ai';
|
|
2
|
+
import { canonical, object } from './protocol.js';
|
|
3
|
+
import { getProviderModel, validateClaudePayload, type ProviderRoute } from './provider-config.js';
|
|
4
|
+
|
|
5
|
+
const common = ['model', 'stream', 'tools', 'tool_choice', 'temperature', 'top_p', 'metadata'];
|
|
6
|
+
const allowedFields: Record<string, string[]> = {
|
|
7
|
+
'anthropic-messages': [
|
|
8
|
+
...common,
|
|
9
|
+
'messages',
|
|
10
|
+
'system',
|
|
11
|
+
'max_tokens',
|
|
12
|
+
'betas',
|
|
13
|
+
'thinking',
|
|
14
|
+
'output_config',
|
|
15
|
+
'top_k',
|
|
16
|
+
'stop_sequences',
|
|
17
|
+
],
|
|
18
|
+
'openai-completions': [
|
|
19
|
+
...common,
|
|
20
|
+
'messages',
|
|
21
|
+
'max_tokens',
|
|
22
|
+
'max_completion_tokens',
|
|
23
|
+
'store',
|
|
24
|
+
'stream_options',
|
|
25
|
+
'reasoning',
|
|
26
|
+
'reasoning_effort',
|
|
27
|
+
'thinking',
|
|
28
|
+
'chat_template_args',
|
|
29
|
+
'enable_thinking',
|
|
30
|
+
'tool_stream',
|
|
31
|
+
'prompt_cache_key',
|
|
32
|
+
'prompt_cache_retention',
|
|
33
|
+
'frequency_penalty',
|
|
34
|
+
'presence_penalty',
|
|
35
|
+
'repetition_penalty',
|
|
36
|
+
'min_p',
|
|
37
|
+
'top_k',
|
|
38
|
+
'stop',
|
|
39
|
+
],
|
|
40
|
+
'mistral-conversations': [
|
|
41
|
+
...common,
|
|
42
|
+
'messages',
|
|
43
|
+
'maxTokens',
|
|
44
|
+
'max_tokens',
|
|
45
|
+
'toolChoice',
|
|
46
|
+
'reasoningEffort',
|
|
47
|
+
'reasoning_effort',
|
|
48
|
+
'promptMode',
|
|
49
|
+
'prompt_mode',
|
|
50
|
+
'promptCacheKey',
|
|
51
|
+
'prompt_cache_key',
|
|
52
|
+
],
|
|
53
|
+
'openai-responses': [
|
|
54
|
+
...common,
|
|
55
|
+
'input',
|
|
56
|
+
'store',
|
|
57
|
+
'max_output_tokens',
|
|
58
|
+
'include',
|
|
59
|
+
'reasoning',
|
|
60
|
+
'prompt_cache_key',
|
|
61
|
+
'prompt_cache_options',
|
|
62
|
+
'prompt_cache_retention',
|
|
63
|
+
'service_tier',
|
|
64
|
+
'text',
|
|
65
|
+
'parallel_tool_calls',
|
|
66
|
+
'instructions',
|
|
67
|
+
],
|
|
68
|
+
'azure-openai-responses': [
|
|
69
|
+
...common,
|
|
70
|
+
'input',
|
|
71
|
+
'store',
|
|
72
|
+
'max_output_tokens',
|
|
73
|
+
'include',
|
|
74
|
+
'reasoning',
|
|
75
|
+
'prompt_cache_key',
|
|
76
|
+
'service_tier',
|
|
77
|
+
'text',
|
|
78
|
+
'parallel_tool_calls',
|
|
79
|
+
],
|
|
80
|
+
'openai-codex-responses': [
|
|
81
|
+
...common,
|
|
82
|
+
'input',
|
|
83
|
+
'store',
|
|
84
|
+
'include',
|
|
85
|
+
'reasoning',
|
|
86
|
+
'prompt_cache_key',
|
|
87
|
+
'service_tier',
|
|
88
|
+
'text',
|
|
89
|
+
'parallel_tool_calls',
|
|
90
|
+
'instructions',
|
|
91
|
+
],
|
|
92
|
+
'google-generative-ai': ['model', 'contents', 'config'],
|
|
93
|
+
'google-vertex': ['model', 'contents', 'config'],
|
|
94
|
+
'bedrock-converse-stream': [
|
|
95
|
+
'modelId',
|
|
96
|
+
'messages',
|
|
97
|
+
'system',
|
|
98
|
+
'inferenceConfig',
|
|
99
|
+
'toolConfig',
|
|
100
|
+
'additionalModelRequestFields',
|
|
101
|
+
'requestMetadata',
|
|
102
|
+
],
|
|
103
|
+
'pi-messages': ['model', 'context', 'options'],
|
|
104
|
+
};
|
|
105
|
+
function keys(value: Record<string, unknown>, allowed: readonly string[]) {
|
|
106
|
+
if (Object.keys(value).some((key) => !allowed.includes(key) && value[key] !== undefined))
|
|
107
|
+
throw new Error('Unsupported provider execution feature');
|
|
108
|
+
}
|
|
109
|
+
function cap(value: unknown, route: ProviderRoute) {
|
|
110
|
+
if (!Number.isSafeInteger(value) || Number(value) < 1 || Number(value) > route.max_tokens)
|
|
111
|
+
throw new Error('Unapproved provider output limit');
|
|
112
|
+
}
|
|
113
|
+
function inventory(
|
|
114
|
+
values: unknown,
|
|
115
|
+
names: string[],
|
|
116
|
+
kind: 'function' | 'google' | 'bedrock' | 'pi',
|
|
117
|
+
) {
|
|
118
|
+
if (!Array.isArray(values)) throw new Error('Provider tool inventory missing');
|
|
119
|
+
const actual: string[] = [];
|
|
120
|
+
for (const value of values) {
|
|
121
|
+
const tool = object(value);
|
|
122
|
+
if (kind === 'google') {
|
|
123
|
+
if (
|
|
124
|
+
Object.keys(tool).some((key) => key !== 'functionDeclarations') ||
|
|
125
|
+
!Array.isArray(tool.functionDeclarations)
|
|
126
|
+
)
|
|
127
|
+
throw new Error('Provider server tools disabled');
|
|
128
|
+
actual.push(...tool.functionDeclarations.map((entry) => String(object(entry).name)));
|
|
129
|
+
} else if (kind === 'bedrock') {
|
|
130
|
+
if (Object.keys(tool).some((key) => key !== 'toolSpec'))
|
|
131
|
+
throw new Error('Provider server tools disabled');
|
|
132
|
+
actual.push(String(object(tool.toolSpec).name));
|
|
133
|
+
} else if (kind === 'function') {
|
|
134
|
+
if (tool.type !== 'function') throw new Error('Provider server tools disabled');
|
|
135
|
+
actual.push(String(tool.name ?? object(tool.function).name));
|
|
136
|
+
} else actual.push(String(tool.name));
|
|
137
|
+
}
|
|
138
|
+
if (canonical(actual.sort()) !== canonical([...names].sort()))
|
|
139
|
+
throw new Error('Unapproved provider tool inventory');
|
|
140
|
+
}
|
|
141
|
+
function lastText(messages: unknown, frame: string, parts: string, role = 'user') {
|
|
142
|
+
if (!Array.isArray(messages) || !messages.length) throw new Error('Provider messages missing');
|
|
143
|
+
const last = object(messages.at(-1));
|
|
144
|
+
const content = last[parts];
|
|
145
|
+
const blocks = Array.isArray(content)
|
|
146
|
+
? content.filter(
|
|
147
|
+
(block) => !(Object.keys(object(block)).length === 1 && object(block).cachePoint),
|
|
148
|
+
)
|
|
149
|
+
: [];
|
|
150
|
+
const text =
|
|
151
|
+
typeof content === 'string' ? content : blocks.length ? object(blocks.at(-1)).text : undefined;
|
|
152
|
+
if (last.role !== role || text !== frame)
|
|
153
|
+
throw new Error('Required task context missing from provider payload');
|
|
154
|
+
}
|
|
155
|
+
/** Validate the API-specific payload after extension hooks. No hosted tools or hidden continuations. */
|
|
156
|
+
export function validateProviderPayload(
|
|
157
|
+
value: unknown,
|
|
158
|
+
route: ProviderRoute,
|
|
159
|
+
frame: string,
|
|
160
|
+
tools: string[],
|
|
161
|
+
allowUncapped = false,
|
|
162
|
+
) {
|
|
163
|
+
const p = object(value),
|
|
164
|
+
api = getProviderModel(route)?.api;
|
|
165
|
+
keys(p, allowedFields[String(api)] ?? []);
|
|
166
|
+
for (const key of [
|
|
167
|
+
'fallbacks',
|
|
168
|
+
'fallback_models',
|
|
169
|
+
'models',
|
|
170
|
+
'route',
|
|
171
|
+
'mcp_servers',
|
|
172
|
+
'container',
|
|
173
|
+
'context_management',
|
|
174
|
+
'previous_response_id',
|
|
175
|
+
'conversation',
|
|
176
|
+
'background',
|
|
177
|
+
'environment',
|
|
178
|
+
'agents',
|
|
179
|
+
'built_in_tools',
|
|
180
|
+
'cachedContent',
|
|
181
|
+
])
|
|
182
|
+
if (p[key] !== undefined && p[key] !== false)
|
|
183
|
+
throw new Error('Unsupported provider execution feature');
|
|
184
|
+
const deployment =
|
|
185
|
+
route.provider === 'azure'
|
|
186
|
+
? (route.connection?.configuration.AZURE_OPENAI_DEPLOYMENT_NAME_MAP?.split(',')
|
|
187
|
+
.map((entry) => entry.trim().split('='))
|
|
188
|
+
.find(([id]) => id === route.model)?.[1]
|
|
189
|
+
?.trim() ?? route.model)
|
|
190
|
+
: route.model;
|
|
191
|
+
if (api === 'anthropic-messages') return validateClaudePayload(p, route, frame, tools);
|
|
192
|
+
if (api === 'google-generative-ai' || api === 'google-vertex') {
|
|
193
|
+
if (p.model !== deployment) throw new Error('Unapproved provider model');
|
|
194
|
+
const config = object(p.config);
|
|
195
|
+
keys(config, [
|
|
196
|
+
'maxOutputTokens',
|
|
197
|
+
'temperature',
|
|
198
|
+
'topP',
|
|
199
|
+
'topK',
|
|
200
|
+
'systemInstruction',
|
|
201
|
+
'tools',
|
|
202
|
+
'toolConfig',
|
|
203
|
+
'thinkingConfig',
|
|
204
|
+
'abortSignal',
|
|
205
|
+
'httpOptions',
|
|
206
|
+
]);
|
|
207
|
+
if (
|
|
208
|
+
config.httpOptions !== undefined &&
|
|
209
|
+
canonical(config.httpOptions) !==
|
|
210
|
+
canonical({ retryOptions: { attempts: 1 }, timeout: 120000 })
|
|
211
|
+
)
|
|
212
|
+
throw new Error('Unapproved Google HTTP controls');
|
|
213
|
+
for (const key of ['cachedContent', 'automaticFunctionCalling', 'responseModalities'])
|
|
214
|
+
if (config[key] !== undefined) throw new Error('Unsupported Google execution feature');
|
|
215
|
+
cap(config.maxOutputTokens, route);
|
|
216
|
+
lastText(p.contents, frame, 'parts');
|
|
217
|
+
inventory(config.tools ?? [], tools, 'google');
|
|
218
|
+
} else if (api === 'bedrock-converse-stream') {
|
|
219
|
+
if (p.modelId !== route.model) throw new Error('Unapproved provider model');
|
|
220
|
+
keys(object(p.inferenceConfig), ['maxTokens', 'temperature', 'topP', 'stopSequences']);
|
|
221
|
+
keys(object(p.additionalModelRequestFields ?? {}), [
|
|
222
|
+
'thinking',
|
|
223
|
+
'reasoningConfig',
|
|
224
|
+
'reasoning_effort',
|
|
225
|
+
'reasoning',
|
|
226
|
+
'output_config',
|
|
227
|
+
'anthropic_beta',
|
|
228
|
+
]);
|
|
229
|
+
cap(object(p.inferenceConfig).maxTokens, route);
|
|
230
|
+
lastText(p.messages, frame, 'content');
|
|
231
|
+
inventory(object(p.toolConfig).tools ?? [], tools, 'bedrock');
|
|
232
|
+
if (p.guardrailConfig || p.promptVariables || p.performanceConfig)
|
|
233
|
+
throw new Error('Unsupported Bedrock execution feature');
|
|
234
|
+
} else if (api === 'pi-messages') {
|
|
235
|
+
if (p.model !== deployment) throw new Error('Unapproved provider model');
|
|
236
|
+
keys(object(p.options), [
|
|
237
|
+
'temperature',
|
|
238
|
+
'maxTokens',
|
|
239
|
+
'reasoning',
|
|
240
|
+
'cacheRetention',
|
|
241
|
+
'sessionId',
|
|
242
|
+
'toolChoice',
|
|
243
|
+
]);
|
|
244
|
+
cap(object(p.options).maxTokens, route);
|
|
245
|
+
const context = object(p.context);
|
|
246
|
+
lastText(context.messages, frame, 'content');
|
|
247
|
+
inventory(getCurrentTools((context as unknown as TranscriptContext).messages), tools, 'pi');
|
|
248
|
+
} else if (
|
|
249
|
+
['openai-responses', 'azure-openai-responses', 'openai-codex-responses'].includes(String(api))
|
|
250
|
+
) {
|
|
251
|
+
if (p.model !== deployment) throw new Error('Unapproved provider model');
|
|
252
|
+
if (p.stream !== true) throw new Error('Provider streaming required');
|
|
253
|
+
if (!allowUncapped || p.max_output_tokens !== undefined) cap(p.max_output_tokens, route);
|
|
254
|
+
lastText(p.input, frame, 'content');
|
|
255
|
+
inventory(p.tools ?? [], tools, 'function');
|
|
256
|
+
if (p.store !== false) throw new Error('Provider storage disabled');
|
|
257
|
+
} else if (api === 'openai-completions' || api === 'mistral-conversations') {
|
|
258
|
+
if (p.model !== deployment || p.stream !== true) throw new Error('Unapproved provider model');
|
|
259
|
+
cap(p.max_completion_tokens ?? p.max_tokens ?? p.maxTokens, route);
|
|
260
|
+
lastText(p.messages, frame, 'content');
|
|
261
|
+
inventory(p.tools ?? [], tools, 'function');
|
|
262
|
+
} else throw new Error('Unsupported provider chat API');
|
|
263
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime';
|
|
3
|
+
|
|
4
|
+
export interface ProviderTransport {
|
|
5
|
+
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
6
|
+
bedrock(request: {
|
|
7
|
+
protocol: string;
|
|
8
|
+
hostname: string;
|
|
9
|
+
path: string;
|
|
10
|
+
method: string;
|
|
11
|
+
body?: unknown;
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
const scope = new AsyncLocalStorage<ProviderTransport>();
|
|
16
|
+
let installed = false;
|
|
17
|
+
let underlyingFetch: typeof globalThis.fetch;
|
|
18
|
+
/** Worker-local SDK interception, scoped to the active model call (not auth refresh). */
|
|
19
|
+
export function installProviderTransport() {
|
|
20
|
+
if (installed) return underlyingFetch;
|
|
21
|
+
installed = true;
|
|
22
|
+
const originalFetch = (underlyingFetch = globalThis.fetch);
|
|
23
|
+
globalThis.fetch = (input, init) =>
|
|
24
|
+
scope.getStore()?.fetch(input, init) ??
|
|
25
|
+
Promise.reject(new Error('Unadmitted worker network request'));
|
|
26
|
+
const send = BedrockRuntimeClient.prototype.send;
|
|
27
|
+
BedrockRuntimeClient.prototype.send = function (
|
|
28
|
+
this: BedrockRuntimeClient,
|
|
29
|
+
...args: Parameters<typeof send>
|
|
30
|
+
) {
|
|
31
|
+
const gate = scope.getStore();
|
|
32
|
+
if (gate) {
|
|
33
|
+
// Pi 1.3.0 doesn't forward maxRetries to this SDK. Disable SDK retries explicitly.
|
|
34
|
+
this.config.retryStrategy = async () => ({
|
|
35
|
+
mode: 'standard',
|
|
36
|
+
retry: async (next, request) => next(request),
|
|
37
|
+
});
|
|
38
|
+
const handler = this.config.requestHandler;
|
|
39
|
+
const handle = handler.handle.bind(handler);
|
|
40
|
+
handler.handle = async (request, options) => {
|
|
41
|
+
await gate.bedrock(request);
|
|
42
|
+
return handle(request, options);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return Reflect.apply(send, this, args);
|
|
46
|
+
} as typeof send;
|
|
47
|
+
return originalFetch;
|
|
48
|
+
}
|
|
49
|
+
export function withProviderTransport<T>(transport: ProviderTransport, fn: () => T): T {
|
|
50
|
+
return scope.run(transport, fn);
|
|
51
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Api } from '@earendil-works/pi-ai';
|
|
2
|
+
|
|
3
|
+
function record(value: unknown): Record<string, unknown> {
|
|
4
|
+
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {};
|
|
5
|
+
}
|
|
6
|
+
const token = (value: unknown) =>
|
|
7
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
8
|
+
/** A normalized zero is not evidence that a provider reported zero usage. */
|
|
9
|
+
export class ProviderUsageReceipt {
|
|
10
|
+
private input = false;
|
|
11
|
+
private output = false;
|
|
12
|
+
observe(api: Api, value: unknown) {
|
|
13
|
+
const event = record(value);
|
|
14
|
+
let input: unknown, output: unknown;
|
|
15
|
+
switch (api) {
|
|
16
|
+
case 'anthropic-messages': {
|
|
17
|
+
const usage = record(
|
|
18
|
+
event.type === 'message_start' ? record(event.message).usage : event.usage,
|
|
19
|
+
);
|
|
20
|
+
input = usage.input_tokens;
|
|
21
|
+
output = event.type === 'message_delta' ? usage.output_tokens : undefined;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case 'google-generative-ai':
|
|
25
|
+
case 'google-vertex': {
|
|
26
|
+
const usage = record(event.usageMetadata);
|
|
27
|
+
input = usage.promptTokenCount;
|
|
28
|
+
output = usage.candidatesTokenCount;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case 'bedrock-converse-stream': {
|
|
32
|
+
const usage = record(record(event.metadata).usage);
|
|
33
|
+
input = usage.inputTokens;
|
|
34
|
+
output = usage.outputTokens;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case 'openai-responses':
|
|
38
|
+
case 'azure-openai-responses':
|
|
39
|
+
case 'openai-codex-responses': {
|
|
40
|
+
const usage = record(record(event.response).usage);
|
|
41
|
+
input = usage.input_tokens;
|
|
42
|
+
output = usage.output_tokens;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
case 'pi-messages': {
|
|
46
|
+
const usage = record(event.usage);
|
|
47
|
+
input = usage.input;
|
|
48
|
+
output = usage.output;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
default: {
|
|
52
|
+
const usage = record(event.usage);
|
|
53
|
+
input = usage.prompt_tokens ?? usage.promptTokens;
|
|
54
|
+
output = usage.completion_tokens ?? usage.completionTokens;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
this.input ||= token(input);
|
|
59
|
+
this.output ||= token(output);
|
|
60
|
+
}
|
|
61
|
+
get complete() {
|
|
62
|
+
return this.input && this.output;
|
|
63
|
+
}
|
|
64
|
+
}
|