@subrouter/opencode 0.4.0 → 0.5.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/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +111 -49
- package/dist/opencode-e2e.test.js +240 -29
- package/dist/plugin.test.js +228 -23
- package/dist/provider.d.ts +34 -15
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +36 -7
- package/package.json +2 -2
- package/src/index.ts +159 -78
- package/src/opencode-e2e.test.ts +258 -30
- package/src/plugin.test.ts +263 -32
- package/src/provider.ts +65 -9
package/src/plugin.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import path from 'node:path'
|
|
|
6
6
|
import util from 'node:util'
|
|
7
7
|
import { afterEach, beforeEach, expect, test } from 'vitest'
|
|
8
8
|
import type { Config, PluginInput } from '@opencode-ai/plugin'
|
|
9
|
-
import { createOpencodeClient
|
|
9
|
+
import { createOpencodeClient } from '@opencode-ai/sdk'
|
|
10
10
|
import {
|
|
11
11
|
addAccount,
|
|
12
12
|
adapters,
|
|
@@ -14,9 +14,17 @@ import {
|
|
|
14
14
|
PROVIDER_DISPLAY_NAME,
|
|
15
15
|
PROVIDER_IDS,
|
|
16
16
|
savePreset,
|
|
17
|
+
setLiveRoute,
|
|
17
18
|
} from '@subrouter/cli'
|
|
18
19
|
import { subrouterAuthPlugin, subrouterPlugin } from './index.ts'
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
appendApplyPatchConstraint,
|
|
22
|
+
applyPatchApiId,
|
|
23
|
+
applyPatchConstraint,
|
|
24
|
+
revealRoutedModel,
|
|
25
|
+
rewritePoweredByModelLine,
|
|
26
|
+
shouldUseApplyPatch,
|
|
27
|
+
} from './provider.ts'
|
|
20
28
|
|
|
21
29
|
const execFile = util.promisify(childProcess.execFile)
|
|
22
30
|
let home: string
|
|
@@ -30,7 +38,6 @@ beforeEach(async () => {
|
|
|
30
38
|
})
|
|
31
39
|
|
|
32
40
|
afterEach(async () => {
|
|
33
|
-
delete process.env.SUBROUTER_HOME
|
|
34
41
|
delete process.env.SUBROUTER_OPENAI_ISSUER_URL
|
|
35
42
|
delete process.env.SUBROUTER_MODELS_DEV_URL
|
|
36
43
|
for (const server of openServers.splice(0)) {
|
|
@@ -56,12 +63,13 @@ async function startFakeModelsDev(
|
|
|
56
63
|
temperature?: boolean
|
|
57
64
|
tool_call?: boolean
|
|
58
65
|
modalities?: { input?: string[]; output?: string[] }
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
limit?: { context?: number; output?: number; input?: number }
|
|
67
|
+
reasoning_options?: Array<{ type: string; values?: Array<string | null> }>
|
|
68
|
+
}
|
|
69
|
+
>
|
|
70
|
+
}
|
|
71
|
+
> = {},
|
|
72
|
+
) {
|
|
65
73
|
const payload = {
|
|
66
74
|
anthropic: { models: {} },
|
|
67
75
|
openai: { models: {} },
|
|
@@ -180,8 +188,8 @@ test('provider log callback forwards only to client.app.log', async () => {
|
|
|
180
188
|
})
|
|
181
189
|
})
|
|
182
190
|
|
|
183
|
-
test('cooldown fallback
|
|
184
|
-
const requests: Array<{ path: string; body:
|
|
191
|
+
test('cooldown fallback persists one ignored notice during the active run', async () => {
|
|
192
|
+
const requests: Array<{ path: string; body: Record<string, unknown> }> = []
|
|
185
193
|
const server = createServer((req, res) => {
|
|
186
194
|
const chunks: Buffer[] = []
|
|
187
195
|
req.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
@@ -205,27 +213,17 @@ test('cooldown fallback creates only a persisted ignored notice after idle', asy
|
|
|
205
213
|
const onCooldownFallback = config.provider?.subrouter?.options?.onCooldownFallback
|
|
206
214
|
expect(onCooldownFallback).toBeTypeOf('function')
|
|
207
215
|
if (typeof onCooldownFallback !== 'function') throw new Error('expected cooldown callback')
|
|
208
|
-
|
|
209
|
-
await onCooldownFallback({
|
|
216
|
+
const notice = {
|
|
210
217
|
sessionID: 'session-1',
|
|
211
218
|
agent: 'build',
|
|
212
219
|
variant: 'high',
|
|
213
220
|
preset: 'work',
|
|
214
|
-
preferred: { provider: 'xai', modelId: 'grok-4.6', retryAfterMs:
|
|
221
|
+
preferred: { provider: 'xai', modelId: 'grok-4.6', retryAfterMs: 250_000 },
|
|
215
222
|
active: { provider: 'openai', modelId: 'gpt-5.6-sol' },
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
preset: 'work',
|
|
221
|
-
preferred: { provider: 'xai', modelId: 'grok-4.6', retryAfterMs: 251_000 },
|
|
222
|
-
active: { provider: 'anthropic', modelId: 'claude-sonnet-5' },
|
|
223
|
-
})
|
|
224
|
-
const event: Event = {
|
|
225
|
-
type: 'session.idle',
|
|
226
|
-
properties: { sessionID: 'session-1' },
|
|
227
|
-
}
|
|
228
|
-
await hooks.event?.({ event })
|
|
223
|
+
} as const
|
|
224
|
+
|
|
225
|
+
await onCooldownFallback(notice)
|
|
226
|
+
await onCooldownFallback(notice)
|
|
229
227
|
|
|
230
228
|
expect(requests).toEqual([
|
|
231
229
|
{
|
|
@@ -238,7 +236,7 @@ test('cooldown fallback creates only a persisted ignored notice after idle', asy
|
|
|
238
236
|
parts: [
|
|
239
237
|
{
|
|
240
238
|
type: 'text',
|
|
241
|
-
text: 'Subrouter: xai/grok-4.6
|
|
239
|
+
text: 'Subrouter: Using openai/gpt-5.6-sol because xai/grok-4.6 is rate limited.',
|
|
242
240
|
ignored: true,
|
|
243
241
|
},
|
|
244
242
|
],
|
|
@@ -327,10 +325,84 @@ test('preset model limits follow the first live candidate', async () => {
|
|
|
327
325
|
input: 900_000,
|
|
328
326
|
output: 128_000,
|
|
329
327
|
})
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('preset reasoning follows the first live candidate', async () => {
|
|
331
|
+
const modelId = adapters.openai.defaultModels[0]
|
|
332
|
+
if (!modelId) throw new Error('openai adapter has no default model')
|
|
333
|
+
process.env.SUBROUTER_MODELS_DEV_URL = await startFakeModelsDev({
|
|
334
|
+
openai: {
|
|
335
|
+
models: {
|
|
336
|
+
[modelId]: {
|
|
337
|
+
id: modelId,
|
|
338
|
+
reasoning: true,
|
|
339
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
340
|
+
limit: { context: 200_000, output: 64_000 },
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
})
|
|
345
|
+
await addAccount({
|
|
346
|
+
provider: 'openai',
|
|
347
|
+
account: {
|
|
348
|
+
type: 'oauth',
|
|
349
|
+
refresh: 'refresh-1',
|
|
350
|
+
access: 'access-1',
|
|
351
|
+
expires: Date.now() + 60_000,
|
|
352
|
+
email: 'a@x.com',
|
|
353
|
+
addedAt: 1,
|
|
354
|
+
lastUsed: 1,
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
await savePreset({ name: 'work', models: [`openai/${modelId}`] })
|
|
358
|
+
|
|
359
|
+
const hooks = await subrouterPlugin(pluginInput)
|
|
360
|
+
const config: Config = {}
|
|
361
|
+
await hooks.config?.(config)
|
|
362
|
+
|
|
363
|
+
expect(config.provider?.subrouter?.models?.work?.reasoning).toBe(true)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test('preset variants follow the first live candidate', async () => {
|
|
367
|
+
const modelId = adapters.openai.defaultModels[0]
|
|
368
|
+
if (!modelId) throw new Error('openai adapter has no default model')
|
|
369
|
+
process.env.SUBROUTER_MODELS_DEV_URL = await startFakeModelsDev({
|
|
370
|
+
openai: {
|
|
371
|
+
models: {
|
|
372
|
+
[modelId]: {
|
|
373
|
+
id: modelId,
|
|
374
|
+
reasoning: true,
|
|
375
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
376
|
+
reasoning_options: [{ type: 'effort', values: ['none', 'low', 'medium', 'high'] }],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
})
|
|
381
|
+
await addAccount({
|
|
382
|
+
provider: 'openai',
|
|
383
|
+
account: {
|
|
384
|
+
type: 'oauth',
|
|
385
|
+
refresh: 'refresh-1',
|
|
386
|
+
access: 'access-1',
|
|
387
|
+
expires: Date.now() + 60_000,
|
|
388
|
+
email: 'a@x.com',
|
|
389
|
+
addedAt: 1,
|
|
390
|
+
lastUsed: 1,
|
|
391
|
+
},
|
|
392
|
+
})
|
|
393
|
+
await savePreset({ name: 'work', models: [`openai/${modelId}#high`] })
|
|
394
|
+
|
|
395
|
+
const hooks = await subrouterPlugin(pluginInput)
|
|
396
|
+
const config: Config = {}
|
|
397
|
+
await hooks.config?.(config)
|
|
398
|
+
|
|
399
|
+
expect(config.provider?.subrouter?.models?.work).toMatchObject({
|
|
400
|
+
variants: {
|
|
401
|
+
none: { reasoningEffort: 'none', reasoningSummary: 'auto' },
|
|
402
|
+
low: { reasoningEffort: 'low', reasoningSummary: 'auto' },
|
|
403
|
+
medium: { reasoningEffort: 'medium', reasoningSummary: 'auto' },
|
|
404
|
+
high: { reasoningEffort: 'high', reasoningSummary: 'auto' },
|
|
405
|
+
},
|
|
334
406
|
})
|
|
335
407
|
})
|
|
336
408
|
|
|
@@ -454,6 +526,87 @@ test('Anthropic-compatible coding plans do not advertise video input', async ()
|
|
|
454
526
|
})
|
|
455
527
|
})
|
|
456
528
|
|
|
529
|
+
test('OpenCode apply_patch matcher follows gpt- and skips gpt-4 and gpt-oss', () => {
|
|
530
|
+
expect(shouldUseApplyPatch('gpt-5.5')).toBe(true)
|
|
531
|
+
expect(shouldUseApplyPatch('gpt-5.4')).toBe(true)
|
|
532
|
+
expect(shouldUseApplyPatch('gpt-5.3-codex')).toBe(true)
|
|
533
|
+
expect(shouldUseApplyPatch('gpt-4.1')).toBe(false)
|
|
534
|
+
expect(shouldUseApplyPatch('gpt-oss-120b')).toBe(false)
|
|
535
|
+
expect(shouldUseApplyPatch('claude-opus-4-6')).toBe(false)
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
test('apply_patch api ids stay unique across presets that share a model', () => {
|
|
539
|
+
const taken = new Set<string>()
|
|
540
|
+
const first = applyPatchApiId({ preset: 'codex-a', modelId: 'gpt-5.5', taken })
|
|
541
|
+
taken.add(first)
|
|
542
|
+
const second = applyPatchApiId({ preset: 'codex-b', modelId: 'gpt-5.5', taken })
|
|
543
|
+
expect(first).toBe('gpt-5.5')
|
|
544
|
+
expect(second).toBe('gpt-5.5:codex-b')
|
|
545
|
+
expect(shouldUseApplyPatch(second)).toBe(true)
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
test('appendApplyPatchConstraint adds the OpenCode GPT line once', () => {
|
|
549
|
+
const system = ['You are powered by the model named gpt-5.5.']
|
|
550
|
+
appendApplyPatchConstraint({ system, modelId: 'gpt-5.5' })
|
|
551
|
+
appendApplyPatchConstraint({ system, modelId: 'gpt-5.5' })
|
|
552
|
+
expect(system).toEqual(['You are powered by the model named gpt-5.5.', applyPatchConstraint('gpt-5.5')])
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
test('appendApplyPatchConstraint skips non-GPT models', () => {
|
|
556
|
+
const system = ['You are powered by the model named claude-opus-4-6.']
|
|
557
|
+
appendApplyPatchConstraint({ system, modelId: 'claude-opus-4-6' })
|
|
558
|
+
expect(system).toEqual(['You are powered by the model named claude-opus-4-6.'])
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
test('openai live candidate spoofs a gpt api id so OpenCode prefers apply_patch', async () => {
|
|
562
|
+
await addAccount({
|
|
563
|
+
provider: 'anthropic',
|
|
564
|
+
account: {
|
|
565
|
+
type: 'oauth',
|
|
566
|
+
refresh: 'refresh-1',
|
|
567
|
+
access: 'access-1',
|
|
568
|
+
expires: Date.now() + 60_000,
|
|
569
|
+
email: 'a@x.com',
|
|
570
|
+
addedAt: 1,
|
|
571
|
+
lastUsed: 1,
|
|
572
|
+
},
|
|
573
|
+
})
|
|
574
|
+
await addAccount({
|
|
575
|
+
provider: 'openai',
|
|
576
|
+
account: {
|
|
577
|
+
type: 'oauth',
|
|
578
|
+
refresh: 'refresh-o',
|
|
579
|
+
access: 'access-o',
|
|
580
|
+
expires: Date.now() + 60_000,
|
|
581
|
+
email: 'o@x.com',
|
|
582
|
+
addedAt: 1,
|
|
583
|
+
lastUsed: 1,
|
|
584
|
+
},
|
|
585
|
+
})
|
|
586
|
+
await savePreset({ name: 'default', models: ['anthropic/claude-opus-4-6'] })
|
|
587
|
+
await savePreset({ name: 'openai-only', models: ['openai/gpt-5.5'] })
|
|
588
|
+
await savePreset({ name: 'codex-b', models: ['openai/gpt-5.5'] })
|
|
589
|
+
|
|
590
|
+
const hooks = await subrouterPlugin(pluginInput)
|
|
591
|
+
const config: Config = {}
|
|
592
|
+
await hooks.config?.(config)
|
|
593
|
+
|
|
594
|
+
expect(config.provider?.subrouter?.models?.work).toBeUndefined()
|
|
595
|
+
expect(config.provider?.subrouter?.models?.['openai-only']).toMatchObject({
|
|
596
|
+
name: 'openai-only',
|
|
597
|
+
id: 'gpt-5.5',
|
|
598
|
+
})
|
|
599
|
+
expect(config.provider?.subrouter?.models?.['codex-b']).toMatchObject({
|
|
600
|
+
name: 'codex-b',
|
|
601
|
+
id: 'gpt-5.5:codex-b',
|
|
602
|
+
})
|
|
603
|
+
expect(config.provider?.subrouter?.models?.default?.id).toBeUndefined()
|
|
604
|
+
expect(config.provider?.subrouter?.options?.presetByApiId).toEqual({
|
|
605
|
+
'gpt-5.5': 'openai-only',
|
|
606
|
+
'gpt-5.5:codex-b': 'codex-b',
|
|
607
|
+
})
|
|
608
|
+
})
|
|
609
|
+
|
|
457
610
|
test('rewrites the OpenCode powered-by line to the routed candidate', () => {
|
|
458
611
|
const system = [
|
|
459
612
|
'You are powered by the model named build. The exact model ID is subrouter/build\n<env>\n Working directory: /tmp\n</env>',
|
|
@@ -492,6 +645,84 @@ test('system transform rewrites the powered-by line to the live candidate', asyn
|
|
|
492
645
|
expect(system[0]).not.toContain('subrouter/build')
|
|
493
646
|
})
|
|
494
647
|
|
|
648
|
+
test('system transform uses the in-flight session route, not the first free preset model', async () => {
|
|
649
|
+
await addAccount({
|
|
650
|
+
provider: 'anthropic',
|
|
651
|
+
account: {
|
|
652
|
+
type: 'oauth',
|
|
653
|
+
refresh: 'refresh-1',
|
|
654
|
+
access: 'access-1',
|
|
655
|
+
expires: Date.now() + 60_000,
|
|
656
|
+
email: 'a@x.com',
|
|
657
|
+
addedAt: 1,
|
|
658
|
+
lastUsed: 1,
|
|
659
|
+
},
|
|
660
|
+
})
|
|
661
|
+
await addAccount({
|
|
662
|
+
provider: 'opencode-go',
|
|
663
|
+
account: { type: 'api', key: 'zen-key', addedAt: 1, lastUsed: 1 },
|
|
664
|
+
})
|
|
665
|
+
await savePreset({
|
|
666
|
+
name: 'default',
|
|
667
|
+
models: ['anthropic/claude-fake', 'opencode-go/fake-model'],
|
|
668
|
+
})
|
|
669
|
+
await setLiveRoute({
|
|
670
|
+
sessionID: 'ses_1',
|
|
671
|
+
preset: 'default',
|
|
672
|
+
provider: 'opencode-go',
|
|
673
|
+
modelId: 'fake-model',
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
const system = [
|
|
677
|
+
'You are powered by the model named default. The exact model ID is subrouter/default',
|
|
678
|
+
]
|
|
679
|
+
await revealRoutedModel({
|
|
680
|
+
providerID: 'subrouter',
|
|
681
|
+
preset: 'default',
|
|
682
|
+
sessionID: 'ses_1',
|
|
683
|
+
system,
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
expect(system[0]).toContain('You are powered by the model named fake-model.')
|
|
687
|
+
expect(system[0]).toContain('The exact model ID is opencode-go/fake-model')
|
|
688
|
+
expect(system[0]).not.toContain('claude-fake')
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
test('system transform appends the apply_patch constraint for a live GPT route', async () => {
|
|
692
|
+
await addAccount({
|
|
693
|
+
provider: 'openai',
|
|
694
|
+
account: {
|
|
695
|
+
type: 'oauth',
|
|
696
|
+
refresh: 'refresh-o',
|
|
697
|
+
access: 'access-o',
|
|
698
|
+
expires: Date.now() + 60_000,
|
|
699
|
+
email: 'o@x.com',
|
|
700
|
+
addedAt: 1,
|
|
701
|
+
lastUsed: 1,
|
|
702
|
+
},
|
|
703
|
+
})
|
|
704
|
+
await savePreset({ name: 'openai-only', models: ['openai/gpt-5.5'] })
|
|
705
|
+
await setLiveRoute({
|
|
706
|
+
sessionID: 'ses_gpt',
|
|
707
|
+
preset: 'openai-only',
|
|
708
|
+
provider: 'openai',
|
|
709
|
+
modelId: 'gpt-5.5',
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
const system = [
|
|
713
|
+
'You are powered by the model named openai-only. The exact model ID is subrouter/openai-only',
|
|
714
|
+
]
|
|
715
|
+
await revealRoutedModel({
|
|
716
|
+
providerID: 'subrouter',
|
|
717
|
+
preset: 'openai-only',
|
|
718
|
+
sessionID: 'ses_gpt',
|
|
719
|
+
system,
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
expect(system[0]).toContain('You are powered by the model named gpt-5.5.')
|
|
723
|
+
expect(system).toContain(applyPatchConstraint('gpt-5.5'))
|
|
724
|
+
})
|
|
725
|
+
|
|
495
726
|
test('system transform leaves other providers unchanged', async () => {
|
|
496
727
|
const original =
|
|
497
728
|
'You are powered by the model named claude-opus-4-6. The exact model ID is anthropic/claude-opus-4-6'
|
package/src/provider.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provider entry loaded by opencode via `provider.subrouter.npm` (file:// URL).
|
|
3
3
|
* OpenCode imports this module, calls the first export starting with `create`,
|
|
4
|
-
* then `sdk.languageModel(modelId)
|
|
5
|
-
*
|
|
4
|
+
* then `sdk.languageModel(modelId)`. For GPT presets that id is a spoofed
|
|
5
|
+
* `gpt-*` api id so OpenCode prefers apply_patch; createSubrouter maps it
|
|
6
|
+
* back to the preset. Also rewrites OpenCode's powered-by identity to the
|
|
7
|
+
* live routed model and appends the GPT apply_patch constraint when needed.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import {
|
|
@@ -11,13 +13,56 @@ import {
|
|
|
11
13
|
OPENAI_WEBSOCKET_SESSION_HEADER,
|
|
12
14
|
OPENAI_WEBSOCKET_TITLE_HEADER,
|
|
13
15
|
PROVIDER_ID,
|
|
14
|
-
|
|
16
|
+
resolveLiveModel,
|
|
17
|
+
ROUTE_AFFINITY_HEADER,
|
|
15
18
|
} from '@subrouter/cli'
|
|
16
19
|
|
|
17
20
|
export { createSubrouter } from '@subrouter/cli'
|
|
18
21
|
|
|
19
22
|
const POWERED_BY_MODEL = /You are powered by the model named [^\n]+/
|
|
20
23
|
|
|
24
|
+
// OpenCode v1 registry.ts and v2 patch.ts: GPT (not gpt-4 / gpt-oss) uses apply_patch.
|
|
25
|
+
export function shouldUseApplyPatch(modelId: string) {
|
|
26
|
+
return modelId.includes('gpt-') && !modelId.includes('oss') && !modelId.includes('gpt-4')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function applyPatchApiId({
|
|
30
|
+
preset,
|
|
31
|
+
modelId,
|
|
32
|
+
taken,
|
|
33
|
+
}: {
|
|
34
|
+
preset: string
|
|
35
|
+
modelId: string
|
|
36
|
+
taken: Set<string>
|
|
37
|
+
}) {
|
|
38
|
+
if (!taken.has(modelId)) return modelId
|
|
39
|
+
return `${modelId}:${preset}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const APPLY_PATCH_GPT =
|
|
43
|
+
'Always use apply_patch for manual code edits. Do not use cat or any other commands when creating or editing files. Formatting commands or bulk edits don\'t need to be done with apply_patch.'
|
|
44
|
+
|
|
45
|
+
const APPLY_PATCH_CODEX =
|
|
46
|
+
'Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase).'
|
|
47
|
+
|
|
48
|
+
export function applyPatchConstraint(modelId: string) {
|
|
49
|
+
if (modelId.includes('codex')) return APPLY_PATCH_CODEX
|
|
50
|
+
return APPLY_PATCH_GPT
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function appendApplyPatchConstraint({
|
|
54
|
+
system,
|
|
55
|
+
modelId,
|
|
56
|
+
}: {
|
|
57
|
+
system: string[]
|
|
58
|
+
modelId: string
|
|
59
|
+
}) {
|
|
60
|
+
if (!shouldUseApplyPatch(modelId)) return
|
|
61
|
+
const constraint = applyPatchConstraint(modelId)
|
|
62
|
+
if (system.some((line) => line.includes('use apply_patch'))) return
|
|
63
|
+
system.push(constraint)
|
|
64
|
+
}
|
|
65
|
+
|
|
21
66
|
export function rewritePoweredByModelLine({
|
|
22
67
|
system,
|
|
23
68
|
candidate,
|
|
@@ -34,37 +79,48 @@ export function rewritePoweredByModelLine({
|
|
|
34
79
|
export async function revealRoutedModel({
|
|
35
80
|
providerID,
|
|
36
81
|
preset,
|
|
82
|
+
sessionID,
|
|
37
83
|
system,
|
|
38
84
|
}: {
|
|
39
85
|
providerID: string
|
|
40
86
|
preset: string
|
|
87
|
+
sessionID?: string
|
|
41
88
|
system: string[]
|
|
42
89
|
}) {
|
|
43
90
|
if (providerID !== PROVIDER_ID) return
|
|
44
|
-
const candidate = await
|
|
91
|
+
const candidate = await resolveLiveModel({ preset, sessionID })
|
|
45
92
|
if (!candidate) return
|
|
46
93
|
rewritePoweredByModelLine({ system, candidate })
|
|
94
|
+
appendApplyPatchConstraint({ system, modelId: candidate.modelId })
|
|
47
95
|
}
|
|
48
96
|
|
|
49
|
-
export function addSubrouterHeaders(
|
|
97
|
+
export function addSubrouterHeaders({
|
|
98
|
+
input,
|
|
99
|
+
output,
|
|
100
|
+
affinityKey = input.message.id,
|
|
101
|
+
}: {
|
|
50
102
|
input: {
|
|
51
103
|
sessionID: string
|
|
52
104
|
agent: string
|
|
53
105
|
model: { providerID: string }
|
|
54
106
|
message: {
|
|
107
|
+
id: string
|
|
55
108
|
agent: string
|
|
56
109
|
model: { providerID: string; modelID: string; variant?: string }
|
|
57
110
|
}
|
|
58
|
-
}
|
|
59
|
-
output: { headers: Record<string, string> }
|
|
60
|
-
|
|
61
|
-
|
|
111
|
+
}
|
|
112
|
+
output: { headers: Record<string, string> }
|
|
113
|
+
affinityKey?: string
|
|
114
|
+
}) {
|
|
115
|
+
if (input.model.providerID !== PROVIDER_ID) return null
|
|
62
116
|
output.headers[OPENAI_WEBSOCKET_SESSION_HEADER] = input.sessionID
|
|
63
117
|
if (input.agent === input.message.agent) {
|
|
118
|
+
output.headers[ROUTE_AFFINITY_HEADER] = affinityKey
|
|
64
119
|
output.headers[OPENCODE_AGENT_HEADER] = input.message.agent
|
|
65
120
|
if (input.message.model.variant) {
|
|
66
121
|
output.headers[OPENCODE_VARIANT_HEADER] = input.message.model.variant
|
|
67
122
|
}
|
|
68
123
|
}
|
|
69
124
|
if (input.agent === 'title') output.headers[OPENAI_WEBSOCKET_TITLE_HEADER] = 'true'
|
|
125
|
+
return output.headers[ROUTE_AFFINITY_HEADER] ?? null
|
|
70
126
|
}
|