@gakim-digital/dexter-bridge 0.5.21 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -35
- package/package.json +19 -5
- package/src/agent.js +1351 -331
- package/src/agentOutput.js +209 -0
- package/src/api.js +48 -1
- package/src/cli.js +267 -39
- package/src/config.js +30 -7
- package/src/framerAgentTools.js +1108 -0
- package/src/harnessMcpServer.js +240 -0
- package/src/harnessTools.js +548 -0
- package/src/logger.js +1 -1
- package/src/nativeSkills.js +295 -0
- package/src/outcomeWorkspace.js +351 -0
- package/src/protocol.js +239 -0
- package/src/providers/acp.js +241 -0
- package/src/providers/codexAppServer.js +1050 -156
- package/src/providers/codexStructuredOutput.js +243 -16
- package/src/providers/directByok.js +197 -0
- package/src/providers/index.js +33 -7
- package/src/providers/openCode.js +607 -0
- package/src/runtimeProfiles.js +284 -0
- package/src/providers/claudeAgentSdk.js +0 -507
|
@@ -1,7 +1,67 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { jsonrepair } from 'jsonrepair';
|
|
3
|
+
import {
|
|
4
|
+
assertOutcomeResponseContract,
|
|
5
|
+
normalizeStructuredResponseContract,
|
|
6
|
+
STRUCTURED_RESPONSE_CONTRACTS,
|
|
7
|
+
} from '../protocol.js';
|
|
8
|
+
|
|
9
|
+
export { STRUCTURED_RESPONSE_CONTRACTS } from '../protocol.js';
|
|
10
|
+
|
|
1
11
|
function isRecord(value) {
|
|
2
12
|
return value && typeof value === 'object' && !Array.isArray(value);
|
|
3
13
|
}
|
|
4
14
|
|
|
15
|
+
const CODEX_TRANSPORT_ERROR_CODES = new Set([
|
|
16
|
+
'CODEX_STRUCTURED_OUTPUT_MALFORMED',
|
|
17
|
+
'CODEX_STRUCTURED_OUTPUT_INVALID',
|
|
18
|
+
'CODEX_TOOL_ARGUMENTS_TRANSPORT_INVALID',
|
|
19
|
+
'CODEX_TOOL_ARGUMENTS_MALFORMED',
|
|
20
|
+
'CODEX_TOOL_ARGUMENTS_NOT_OBJECT',
|
|
21
|
+
'APP_HARNESS_RESULT_INVALID',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function sha256(value) {
|
|
25
|
+
return createHash('sha256')
|
|
26
|
+
.update(String(value || ''))
|
|
27
|
+
.digest('hex');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function diagnosticPreview(value, limit = 1_000) {
|
|
31
|
+
return String(value || '')
|
|
32
|
+
.slice(0, limit)
|
|
33
|
+
.replace(
|
|
34
|
+
/((?:api[-_]?key|authorization|bearer|secret|password|token|credential)(?:\\?"?\s*[:=]\s*\\?"?))[^,}\n"]+/gi,
|
|
35
|
+
'$1[redacted]',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function transportError(code, message, details = {}) {
|
|
40
|
+
return Object.assign(new Error(message), {
|
|
41
|
+
code,
|
|
42
|
+
retryable: true,
|
|
43
|
+
codexTransportRecoverable: true,
|
|
44
|
+
...details,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseJsonObject(source) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(source);
|
|
51
|
+
} catch (cause) {
|
|
52
|
+
const trimmed = String(source || '').trim();
|
|
53
|
+
const completeContainer =
|
|
54
|
+
(trimmed.startsWith('{') && trimmed.endsWith('}'))
|
|
55
|
+
|| (trimmed.startsWith('[') && trimmed.endsWith(']'));
|
|
56
|
+
if (!completeContainer) throw cause;
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(jsonrepair(trimmed));
|
|
59
|
+
} catch {
|
|
60
|
+
throw cause;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
5
65
|
function finiteInteger(value) {
|
|
6
66
|
const parsed = Number(value);
|
|
7
67
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
|
|
@@ -36,7 +96,7 @@ function toolNames(outputSchema) {
|
|
|
36
96
|
* tool catalog remains in the prompt and the server validates decoded arguments
|
|
37
97
|
* before execution.
|
|
38
98
|
*/
|
|
39
|
-
export function codexTransportOutputSchema(outputSchema) {
|
|
99
|
+
export function codexTransportOutputSchema(outputSchema, { toolName, requireSingleToolCall = false } = {}) {
|
|
40
100
|
if (!isRecord(outputSchema)) {
|
|
41
101
|
throw new Error('Codex model turn requires an output schema.');
|
|
42
102
|
}
|
|
@@ -47,7 +107,8 @@ export function codexTransportOutputSchema(outputSchema) {
|
|
|
47
107
|
throw new Error('Codex model turn output schema is missing its response envelope.');
|
|
48
108
|
}
|
|
49
109
|
|
|
50
|
-
const
|
|
110
|
+
const availableNames = toolNames(outputSchema);
|
|
111
|
+
const names = typeof toolName === 'string' && availableNames.includes(toolName) ? [toolName] : availableNames;
|
|
51
112
|
const minItems = finiteInteger(sourceToolCalls.minItems);
|
|
52
113
|
const maxItems = finiteInteger(sourceToolCalls.maxItems);
|
|
53
114
|
const finishReasonEnum = Array.isArray(sourceFinishReason.enum)
|
|
@@ -60,8 +121,12 @@ export function codexTransportOutputSchema(outputSchema) {
|
|
|
60
121
|
text: { type: 'string' },
|
|
61
122
|
toolCalls: {
|
|
62
123
|
type: 'array',
|
|
63
|
-
...(
|
|
64
|
-
|
|
124
|
+
...(requireSingleToolCall
|
|
125
|
+
? { minItems: 1, maxItems: 1 }
|
|
126
|
+
: {
|
|
127
|
+
...(minItems === undefined ? {} : { minItems }),
|
|
128
|
+
...(maxItems === undefined ? {} : { maxItems }),
|
|
129
|
+
}),
|
|
65
130
|
items: {
|
|
66
131
|
type: 'object',
|
|
67
132
|
properties: {
|
|
@@ -82,9 +147,11 @@ export function codexTransportOutputSchema(outputSchema) {
|
|
|
82
147
|
},
|
|
83
148
|
finishReason: {
|
|
84
149
|
type: 'string',
|
|
85
|
-
enum:
|
|
86
|
-
?
|
|
87
|
-
:
|
|
150
|
+
enum: requireSingleToolCall
|
|
151
|
+
? ['tool_calls']
|
|
152
|
+
: finishReasonEnum.length
|
|
153
|
+
? finishReasonEnum
|
|
154
|
+
: ['tool_calls', 'stop', 'length'],
|
|
88
155
|
},
|
|
89
156
|
},
|
|
90
157
|
required: ['text', 'toolCalls', 'finishReason'],
|
|
@@ -92,6 +159,22 @@ export function codexTransportOutputSchema(outputSchema) {
|
|
|
92
159
|
};
|
|
93
160
|
}
|
|
94
161
|
|
|
162
|
+
export function codexStructuredOutputSchema(
|
|
163
|
+
outputSchema,
|
|
164
|
+
{
|
|
165
|
+
responseContract = STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
166
|
+
toolName,
|
|
167
|
+
requireSingleToolCall = false,
|
|
168
|
+
} = {},
|
|
169
|
+
) {
|
|
170
|
+
return normalizeStructuredResponseContract(responseContract) === STRUCTURED_RESPONSE_CONTRACTS.OUTCOME
|
|
171
|
+
? assertOutcomeResponseContract(responseContract, outputSchema)
|
|
172
|
+
: codexTransportOutputSchema(outputSchema, {
|
|
173
|
+
toolName,
|
|
174
|
+
requireSingleToolCall,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
95
178
|
export function codexTransportPrompt(prompt) {
|
|
96
179
|
return [
|
|
97
180
|
String(prompt || ''),
|
|
@@ -99,36 +182,137 @@ export function codexTransportPrompt(prompt) {
|
|
|
99
182
|
'Codex structured-output transport:',
|
|
100
183
|
'In the final JSON response, encode each toolCalls[].arguments value as a JSON string.',
|
|
101
184
|
'The decoded string must be one JSON object matching the exact selected tool schema in the Tools catalog.',
|
|
185
|
+
'Keep large code mutations bounded: prefer one cohesive file per tool call and continue remaining files in later model turns.',
|
|
102
186
|
'Do not omit the id, name, arguments, text, toolCalls, or finishReason fields.',
|
|
103
187
|
].join('\n');
|
|
104
188
|
}
|
|
105
189
|
|
|
190
|
+
export function codexStructuredOutputPrompt(
|
|
191
|
+
prompt,
|
|
192
|
+
responseContract = STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
193
|
+
) {
|
|
194
|
+
return normalizeStructuredResponseContract(responseContract) === STRUCTURED_RESPONSE_CONTRACTS.OUTCOME
|
|
195
|
+
? String(prompt || '')
|
|
196
|
+
: codexTransportPrompt(prompt);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function codexTransportCorrectionPrompt(error) {
|
|
200
|
+
const callNumber = Number.isInteger(error?.toolCallIndex) ? error.toolCallIndex + 1 : 1;
|
|
201
|
+
const toolName = typeof error?.toolName === 'string' && error.toolName ? ` for the "${error.toolName}" tool` : '';
|
|
202
|
+
return [
|
|
203
|
+
'Transport correction only.',
|
|
204
|
+
`Your previous structured response was rejected before any tool executed because tool call ${callNumber}${toolName} could not be decoded.`,
|
|
205
|
+
...(typeof error?.parseMessage === 'string' && error.parseMessage ? [`Parser error: ${error.parseMessage}`] : []),
|
|
206
|
+
'Regenerate only the same intended tool call. Do not redesign, reread files, source images, or choose a different action.',
|
|
207
|
+
'If the intended call contains source code, return one cohesive file mutation only; later files can be sent in later model turns.',
|
|
208
|
+
'Return exactly one JSON response object with text:"", one toolCalls item, and finishReason:"tool_calls".',
|
|
209
|
+
'toolCalls[0].arguments must be a JSON string produced by JSON.stringify on one argument object.',
|
|
210
|
+
'Escape every quote, backslash, newline, and control character inside that string. Do not use markdown or commentary.',
|
|
211
|
+
].join('\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function codexStructuredOutputCorrectionPrompt(
|
|
215
|
+
error,
|
|
216
|
+
responseContract = STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
217
|
+
) {
|
|
218
|
+
if (normalizeStructuredResponseContract(responseContract) !== STRUCTURED_RESPONSE_CONTRACTS.OUTCOME) {
|
|
219
|
+
return codexTransportCorrectionPrompt(error);
|
|
220
|
+
}
|
|
221
|
+
return [
|
|
222
|
+
'Transport correction only.',
|
|
223
|
+
'Your previous harness outcome response could not be decoded.',
|
|
224
|
+
...(typeof error?.parseMessage === 'string' && error.parseMessage
|
|
225
|
+
? [`Parser error: ${error.parseMessage}`]
|
|
226
|
+
: []),
|
|
227
|
+
'Continue from the existing workspace. Do not redesign or repeat completed work.',
|
|
228
|
+
'Return exactly one JSON outcome object with status, summary, checks, and blockedReason.',
|
|
229
|
+
'Do not use markdown or a model-turn toolCalls envelope.',
|
|
230
|
+
].join('\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function isCodexTransportRecoverableError(error) {
|
|
234
|
+
return Boolean(error?.codexTransportRecoverable === true || CODEX_TRANSPORT_ERROR_CODES.has(error?.code));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function codexTransportErrorDiagnostics(error) {
|
|
238
|
+
return {
|
|
239
|
+
code: error?.code,
|
|
240
|
+
toolCallIndex: error?.toolCallIndex,
|
|
241
|
+
toolName: error?.toolName,
|
|
242
|
+
argumentsLength: error?.argumentsLength,
|
|
243
|
+
argumentsSha256: error?.argumentsSha256,
|
|
244
|
+
argumentsPreview: error?.argumentsPreview,
|
|
245
|
+
outputLength: error?.outputLength,
|
|
246
|
+
outputSha256: error?.outputSha256,
|
|
247
|
+
outputPreview: error?.outputPreview,
|
|
248
|
+
parseMessage: error?.parseMessage,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
106
252
|
export function decodeCodexTransportOutput(text) {
|
|
253
|
+
const source = String(text || '').trim();
|
|
254
|
+
const outputDiagnostics = {
|
|
255
|
+
outputLength: source.length,
|
|
256
|
+
outputSha256: sha256(source),
|
|
257
|
+
outputPreview: diagnosticPreview(source),
|
|
258
|
+
};
|
|
107
259
|
let parsed;
|
|
108
260
|
try {
|
|
109
|
-
parsed =
|
|
110
|
-
} catch {
|
|
111
|
-
throw
|
|
261
|
+
parsed = parseJsonObject(source);
|
|
262
|
+
} catch (cause) {
|
|
263
|
+
throw transportError('CODEX_STRUCTURED_OUTPUT_MALFORMED', 'Codex returned malformed structured output.', {
|
|
264
|
+
...outputDiagnostics,
|
|
265
|
+
parseMessage: cause instanceof Error ? cause.message.slice(0, 500) : undefined,
|
|
266
|
+
});
|
|
112
267
|
}
|
|
113
268
|
if (!isRecord(parsed) || !Array.isArray(parsed.toolCalls)) {
|
|
114
|
-
throw
|
|
269
|
+
throw transportError(
|
|
270
|
+
'CODEX_STRUCTURED_OUTPUT_INVALID',
|
|
271
|
+
'Codex returned an invalid model-turn response envelope.',
|
|
272
|
+
outputDiagnostics,
|
|
273
|
+
);
|
|
115
274
|
}
|
|
116
275
|
|
|
117
276
|
const toolCalls = parsed.toolCalls.map((call, index) => {
|
|
118
277
|
if (!isRecord(call) || typeof call.arguments !== 'string') {
|
|
119
|
-
throw
|
|
278
|
+
throw transportError(
|
|
279
|
+
'CODEX_TOOL_ARGUMENTS_TRANSPORT_INVALID',
|
|
120
280
|
`Codex tool call ${index + 1} did not use the JSON-string arguments transport.`,
|
|
281
|
+
{
|
|
282
|
+
...outputDiagnostics,
|
|
283
|
+
toolCallIndex: index,
|
|
284
|
+
toolName: isRecord(call) ? call.name : undefined,
|
|
285
|
+
},
|
|
121
286
|
);
|
|
122
287
|
}
|
|
123
288
|
|
|
289
|
+
const argumentDiagnostics = {
|
|
290
|
+
...outputDiagnostics,
|
|
291
|
+
toolCallIndex: index,
|
|
292
|
+
toolName: typeof call.name === 'string' ? call.name : undefined,
|
|
293
|
+
argumentsLength: call.arguments.length,
|
|
294
|
+
argumentsSha256: sha256(call.arguments),
|
|
295
|
+
argumentsPreview: diagnosticPreview(call.arguments),
|
|
296
|
+
};
|
|
124
297
|
let args;
|
|
125
298
|
try {
|
|
126
|
-
args =
|
|
127
|
-
} catch {
|
|
128
|
-
throw
|
|
299
|
+
args = parseJsonObject(call.arguments);
|
|
300
|
+
} catch (cause) {
|
|
301
|
+
throw transportError(
|
|
302
|
+
'CODEX_TOOL_ARGUMENTS_MALFORMED',
|
|
303
|
+
`Codex tool call ${index + 1} contained malformed JSON arguments.`,
|
|
304
|
+
{
|
|
305
|
+
...argumentDiagnostics,
|
|
306
|
+
parseMessage: cause instanceof Error ? cause.message.slice(0, 500) : undefined,
|
|
307
|
+
},
|
|
308
|
+
);
|
|
129
309
|
}
|
|
130
310
|
if (!isRecord(args)) {
|
|
131
|
-
throw
|
|
311
|
+
throw transportError(
|
|
312
|
+
'CODEX_TOOL_ARGUMENTS_NOT_OBJECT',
|
|
313
|
+
`Codex tool call ${index + 1} arguments must decode to an object.`,
|
|
314
|
+
argumentDiagnostics,
|
|
315
|
+
);
|
|
132
316
|
}
|
|
133
317
|
return {
|
|
134
318
|
...call,
|
|
@@ -141,3 +325,46 @@ export function decodeCodexTransportOutput(text) {
|
|
|
141
325
|
toolCalls,
|
|
142
326
|
});
|
|
143
327
|
}
|
|
328
|
+
|
|
329
|
+
export function decodeCodexStructuredOutput(
|
|
330
|
+
text,
|
|
331
|
+
responseContract = STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
332
|
+
) {
|
|
333
|
+
if (normalizeStructuredResponseContract(responseContract) !== STRUCTURED_RESPONSE_CONTRACTS.OUTCOME) {
|
|
334
|
+
return decodeCodexTransportOutput(text);
|
|
335
|
+
}
|
|
336
|
+
const source = String(text || '').trim();
|
|
337
|
+
const outputDiagnostics = {
|
|
338
|
+
outputLength: source.length,
|
|
339
|
+
outputSha256: sha256(source),
|
|
340
|
+
outputPreview: diagnosticPreview(source),
|
|
341
|
+
};
|
|
342
|
+
let parsed;
|
|
343
|
+
try {
|
|
344
|
+
parsed = parseJsonObject(source);
|
|
345
|
+
} catch (cause) {
|
|
346
|
+
throw transportError(
|
|
347
|
+
'APP_HARNESS_RESULT_INVALID',
|
|
348
|
+
'Codex returned malformed harness outcome JSON.',
|
|
349
|
+
{
|
|
350
|
+
...outputDiagnostics,
|
|
351
|
+
parseMessage:
|
|
352
|
+
cause instanceof Error ? cause.message.slice(0, 500) : undefined,
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (
|
|
357
|
+
!isRecord(parsed)
|
|
358
|
+
|| typeof parsed.status !== 'string'
|
|
359
|
+
|| typeof parsed.summary !== 'string'
|
|
360
|
+
|| !Array.isArray(parsed.checks)
|
|
361
|
+
|| !Object.prototype.hasOwnProperty.call(parsed, 'blockedReason')
|
|
362
|
+
) {
|
|
363
|
+
throw transportError(
|
|
364
|
+
'APP_HARNESS_RESULT_INVALID',
|
|
365
|
+
'Codex returned an invalid harness outcome response.',
|
|
366
|
+
outputDiagnostics,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return JSON.stringify(parsed);
|
|
370
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { streamText } from 'ai';
|
|
2
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
3
|
+
import { createDeepSeek } from '@ai-sdk/deepseek';
|
|
4
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
5
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
6
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
7
|
+
import { createXai } from '@ai-sdk/xai';
|
|
8
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
9
|
+
import { normalizeCompanionTokenUsage } from '../agentOutput.js';
|
|
10
|
+
|
|
11
|
+
const PROVIDERS = new Set([
|
|
12
|
+
'openai',
|
|
13
|
+
'anthropic',
|
|
14
|
+
'google',
|
|
15
|
+
'openrouter',
|
|
16
|
+
'deepseek',
|
|
17
|
+
'xai',
|
|
18
|
+
'openai-compatible',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function text(value, maximum = 4096) {
|
|
22
|
+
return typeof value === 'string' && value.trim()
|
|
23
|
+
? value.trim().slice(0, maximum)
|
|
24
|
+
: '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeBaseUrl(value) {
|
|
28
|
+
const raw = text(value, 2048);
|
|
29
|
+
if (!raw) return undefined;
|
|
30
|
+
const parsed = new URL(raw);
|
|
31
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
32
|
+
const loopback =
|
|
33
|
+
hostname === 'localhost'
|
|
34
|
+
|| hostname === 'localhost.'
|
|
35
|
+
|| hostname === '127.0.0.1'
|
|
36
|
+
|| hostname === '::1';
|
|
37
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
38
|
+
throw new Error('Local provider URLs cannot contain credentials, query strings, or fragments.');
|
|
39
|
+
}
|
|
40
|
+
if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && loopback)) {
|
|
41
|
+
throw new Error('Local provider URLs must use HTTPS, except for loopback development endpoints.');
|
|
42
|
+
}
|
|
43
|
+
return parsed.toString().replace(/\/+$/, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function languageModel(credentials, model) {
|
|
47
|
+
const provider = text(credentials?.provider, 80).toLowerCase();
|
|
48
|
+
const apiKey = text(credentials?.apiKey, 8192);
|
|
49
|
+
const baseURL = normalizeBaseUrl(credentials?.baseURL);
|
|
50
|
+
if (!PROVIDERS.has(provider)) {
|
|
51
|
+
throw new Error(`Unsupported local BYOK provider "${provider || 'unknown'}".`);
|
|
52
|
+
}
|
|
53
|
+
if (!apiKey) throw new Error('The local BYOK profile has no API key.');
|
|
54
|
+
if (!text(model, 255)) throw new Error('The local BYOK profile has no model.');
|
|
55
|
+
|
|
56
|
+
switch (provider) {
|
|
57
|
+
case 'openai':
|
|
58
|
+
return createOpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) }).chat(model);
|
|
59
|
+
case 'anthropic':
|
|
60
|
+
return createAnthropic({ apiKey, ...(baseURL ? { baseURL } : {}) }).messages(model);
|
|
61
|
+
case 'google':
|
|
62
|
+
return createGoogleGenerativeAI({ apiKey, ...(baseURL ? { baseURL } : {}) }).chat(model);
|
|
63
|
+
case 'openrouter':
|
|
64
|
+
return createOpenRouter({
|
|
65
|
+
apiKey,
|
|
66
|
+
appName: 'InstaWebAI',
|
|
67
|
+
compatibility: 'strict',
|
|
68
|
+
}).chat(model);
|
|
69
|
+
case 'deepseek':
|
|
70
|
+
return createDeepSeek({ apiKey, ...(baseURL ? { baseURL } : {}) }).chat(model);
|
|
71
|
+
case 'xai':
|
|
72
|
+
return createXai({ apiKey, ...(baseURL ? { baseURL } : {}) }).chat(model);
|
|
73
|
+
case 'openai-compatible':
|
|
74
|
+
if (!baseURL) throw new Error('OpenAI-compatible local BYOK profiles require a base URL.');
|
|
75
|
+
return createOpenAICompatible({
|
|
76
|
+
name: 'instawebai-local-byok',
|
|
77
|
+
apiKey,
|
|
78
|
+
baseURL,
|
|
79
|
+
}).chatModel(model);
|
|
80
|
+
default:
|
|
81
|
+
throw new Error(`Unsupported local BYOK provider "${provider}".`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createDirectByokAdapter({
|
|
86
|
+
profile,
|
|
87
|
+
credentials,
|
|
88
|
+
trace,
|
|
89
|
+
} = {}) {
|
|
90
|
+
if (!profile?.id) throw new Error('A local BYOK runtime profile is required.');
|
|
91
|
+
const activeTurns = new Map();
|
|
92
|
+
|
|
93
|
+
async function runModelTurn({
|
|
94
|
+
runId,
|
|
95
|
+
sessionId,
|
|
96
|
+
prompt,
|
|
97
|
+
model,
|
|
98
|
+
timeoutMs = 180_000,
|
|
99
|
+
maxDurationMs = 15 * 60_000,
|
|
100
|
+
onProgress,
|
|
101
|
+
} = {}) {
|
|
102
|
+
const turnId = sessionId || runId;
|
|
103
|
+
if (!turnId) throw new Error('Local BYOK model turn requires a session id.');
|
|
104
|
+
if (activeTurns.has(turnId)) throw new Error('Local BYOK model turn is already active.');
|
|
105
|
+
const abortController = new AbortController();
|
|
106
|
+
let deadlineReached = false;
|
|
107
|
+
const timer = setTimeout(() => {
|
|
108
|
+
deadlineReached = true;
|
|
109
|
+
abortController.abort();
|
|
110
|
+
}, maxDurationMs);
|
|
111
|
+
timer.unref?.();
|
|
112
|
+
activeTurns.set(turnId, abortController);
|
|
113
|
+
try {
|
|
114
|
+
const selectedModel = text(model, 255) || profile.models?.[0]?.invocationName || profile.models?.[0]?.id;
|
|
115
|
+
onProgress?.({
|
|
116
|
+
kind: 'turn_started',
|
|
117
|
+
provider: `local-byok:${credentials.provider}`,
|
|
118
|
+
occurredAt: new Date().toISOString(),
|
|
119
|
+
});
|
|
120
|
+
const result = streamText({
|
|
121
|
+
model: languageModel(credentials, selectedModel),
|
|
122
|
+
prompt: String(prompt || ''),
|
|
123
|
+
maxOutputTokens: 6_000,
|
|
124
|
+
abortSignal: abortController.signal,
|
|
125
|
+
onChunk({ chunk }) {
|
|
126
|
+
const type = String(chunk?.type || '');
|
|
127
|
+
onProgress?.({
|
|
128
|
+
kind: /reasoning/i.test(type) ? 'reasoning_delta' : 'output_delta',
|
|
129
|
+
provider: `local-byok:${credentials.provider}`,
|
|
130
|
+
occurredAt: new Date().toISOString(),
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const [resultText, usage, response] = await Promise.all([
|
|
135
|
+
result.text,
|
|
136
|
+
result.usage,
|
|
137
|
+
result.response,
|
|
138
|
+
]);
|
|
139
|
+
return {
|
|
140
|
+
text: resultText || '',
|
|
141
|
+
model: response?.modelId || selectedModel,
|
|
142
|
+
tokenUsage: normalizeCompanionTokenUsage(usage || {}),
|
|
143
|
+
usageAvailable: Boolean(usage),
|
|
144
|
+
usageSource: `local-byok:${credentials.provider}`,
|
|
145
|
+
usageAccuracy: 'reported',
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (deadlineReached) {
|
|
149
|
+
throw Object.assign(
|
|
150
|
+
new Error(`The model turn exceeded the ${maxDurationMs}ms safety limit.`),
|
|
151
|
+
{
|
|
152
|
+
code: 'APP_AGENT_TURN_HARD_TIMEOUT',
|
|
153
|
+
retryable: true,
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (abortController.signal.aborted) {
|
|
158
|
+
throw Object.assign(new Error('Local BYOK model turn was cancelled.'), {
|
|
159
|
+
code: 'RUN_CANCELLED',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
} finally {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
activeTurns.delete(turnId);
|
|
166
|
+
trace?.info?.('local_byok_turn_closed', {
|
|
167
|
+
profileId: profile.id,
|
|
168
|
+
sessionId: turnId,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function cancel(sessionId) {
|
|
174
|
+
activeTurns.get(sessionId)?.abort();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function close() {
|
|
178
|
+
for (const controller of activeTurns.values()) controller.abort();
|
|
179
|
+
activeTurns.clear();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
id: profile.id,
|
|
184
|
+
driverKind: 'direct-byok',
|
|
185
|
+
label: profile.label || 'Local API key',
|
|
186
|
+
detect: async () => ({
|
|
187
|
+
ok: true,
|
|
188
|
+
installed: true,
|
|
189
|
+
signedIn: Boolean(credentials?.apiKey),
|
|
190
|
+
agent: 'direct-byok',
|
|
191
|
+
}),
|
|
192
|
+
models: async () => profile.models || [],
|
|
193
|
+
runModelTurn,
|
|
194
|
+
cancel,
|
|
195
|
+
close,
|
|
196
|
+
};
|
|
197
|
+
}
|
package/src/providers/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createCodexAppServerAdapter } from './codexAppServer.js';
|
|
2
|
+
import { createDirectByokAdapter } from './directByok.js';
|
|
3
|
+
import { createOpenCodeAdapter } from './openCode.js';
|
|
4
|
+
import { createAcpAdapter } from './acp.js';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Local agent adapters.
|
|
@@ -22,23 +25,46 @@ import { createCodexAppServerAdapter } from './codexAppServer.js';
|
|
|
22
25
|
* @property {Promise<{ok:boolean}>} completed Resolves when sign-in finishes.
|
|
23
26
|
*
|
|
24
27
|
* @typedef {Object} LocalAgentAdapter
|
|
25
|
-
* @property {
|
|
28
|
+
* @property {string} id
|
|
26
29
|
* @property {() => Promise<ProviderStatus>} detect
|
|
27
30
|
* @property {(() => Promise<AuthChallenge>)=} authenticate
|
|
28
31
|
* @property {() => Promise<Array<object>>} models
|
|
29
|
-
* @property {(input: object) => Promise<{text: string, tokenUsage?: object}>} runModelTurn
|
|
32
|
+
* @property {(input: object & {responseContract?: 'model-turn'}) => Promise<{text: string, tokenUsage?: object}>} runModelTurn
|
|
33
|
+
* @property {((input: object & {responseContract: 'outcome', outputSchema: object}) => Promise<{text: string, tokenUsage?: object}>)=} runOutcome
|
|
30
34
|
* @property {(sessionId: string) => Promise<void>} cancel
|
|
31
35
|
* @property {(() => Promise<void>)=} logout
|
|
32
36
|
*/
|
|
33
37
|
|
|
34
|
-
export const
|
|
35
|
-
codex:
|
|
38
|
+
export const DRIVER_FACTORIES = {
|
|
39
|
+
'codex-app-server': (profile, options) =>
|
|
40
|
+
createCodexAppServerAdapter(options),
|
|
41
|
+
'direct-byok': (profile, options) =>
|
|
42
|
+
createDirectByokAdapter({
|
|
43
|
+
...options,
|
|
44
|
+
profile,
|
|
45
|
+
credentials: options.credentials || profile.credentials,
|
|
46
|
+
}),
|
|
47
|
+
opencode: (profile, options) =>
|
|
48
|
+
createOpenCodeAdapter({ ...options, profile }),
|
|
49
|
+
acp: (profile, options) =>
|
|
50
|
+
createAcpAdapter({ ...options, profile }),
|
|
36
51
|
};
|
|
37
52
|
|
|
38
53
|
/**
|
|
39
54
|
* Returns an adapter for the agent, or null when the agent has no adapter.
|
|
40
55
|
*/
|
|
41
|
-
export function createLocalAgentAdapter(
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
export function createLocalAgentAdapter(profileOrAgent, options = {}) {
|
|
57
|
+
const profile =
|
|
58
|
+
typeof profileOrAgent === 'string'
|
|
59
|
+
? profileOrAgent === 'codex'
|
|
60
|
+
? { id: 'codex:default', driver: 'codex-app-server' }
|
|
61
|
+
: null
|
|
62
|
+
: profileOrAgent;
|
|
63
|
+
if (!profile?.driver) return null;
|
|
64
|
+
const factory = DRIVER_FACTORIES[profile.driver];
|
|
65
|
+
if (!factory) return null;
|
|
66
|
+
const adapter = factory(profile, options);
|
|
67
|
+
if (adapter && !adapter.driverKind) adapter.driverKind = profile.driver;
|
|
68
|
+
if (adapter && profile.id) adapter.profileId = profile.id;
|
|
69
|
+
return adapter;
|
|
44
70
|
}
|