feat(models): bridge Chat Completions and Responses through Claude runtime

This commit is contained in:
Codex committed 2026-10-21 18:19:03 +09:00
1 parent 8fcfb69408
commit d773961168
21 files changed
+1346 -68

No files matched your search

+212
View File
@@ -0,0 +1,212 @@
import assert from 'node:assert/strict'
import { mkdtemp, readFile, stat, writeFile, rm } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { createConversionContext } from '../src/main/model-bridge/history'
import { createServer } from 'node:http'
import type { AddressInfo } from 'node:net'
import { acquireModelBridge } from '../src/main/model-bridge'
import { convertRequest, convertResponse, type ConversionContext } from '../src/main/model-bridge/protocol'
import { readSSE } from '../src/main/model-bridge/stream'
import type { ModelApi, ResolvedModel } from '../src/shared/models'
let checks = 0
function check(value: unknown, message: string) { assert.ok(value, message); checks++ }
const context = (): ConversionContext => ({ responsesHistory: new Map() })
function model(api: ModelApi, baseUrl = 'http://127.0.0.1:1/v1'): ResolvedModel {
return { profile: { id: api, model: 'offline-model', name: 'Offline', providerId: 'offline', baseUrl, hasApiKey: false, keyStorage: 'none', supportsImages: true, supportsToolCall: true }, config: { api, model: 'offline-model', baseUrl, apiKey: 'mock-upstream-secret', temperature: 0.2, maxToolRounds: 3 } }
}
const request = { model: 'attacker-model', max_tokens: 128, system: [{ type: 'text', text: 'System' }], messages: [{ role: 'user', content: [{ type: 'text', text: 'hello' }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'aGk=' } }] }, { role: 'assistant', content: [{ type: 'tool_use', id: 'call_1', name: 'echo', input: { value: 'ok' } }] }, { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_1', content: 'ok' }, { type: 'text', text: 'continue' }] }], tools: [{ name: 'echo', input_schema: { type: 'object', properties: { value: { type: 'string' } } } }], tool_choice: { type: 'any', disable_parallel_tool_use: true } }
for (const api of ['openai-completions', 'openai-responses'] as const) {
const value = convertRequest(request, model(api), context())
check(value.model === 'offline-model', `${api}: immutable model`)
check(value.tool_choice === 'required' && value.parallel_tool_calls === false, `${api}: tool choice`)
check(!('apiKey' in value), `${api}: no credentials in payload`)
if (api === 'openai-responses') {
check(value.store === false && !value.previous_response_id, 'Responses stateless')
check(value.input.some((x: any) => x.type === 'function_call') && value.input.some((x: any) => x.type === 'function_call_output'), 'Responses full tool history')
check(value.input[1].content[1].image_url === 'data:image/png;base64,aGk=', 'Responses image')
} else {
check(value.messages[2].tool_calls[0].function.arguments === '{"value":"ok"}', 'Chat tool history')
check(value.messages[3].role === 'tool' && value.messages[4].role === 'user', 'Chat tool result order')
}
assert.throws(() => convertRequest({ ...request, messages: [{ role: 'user', content: [{ type: 'document' }] }] }, model(api), context()), /Unsupported/); checks++
assert.throws(() => convertRequest({ ...request, messages: [{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'x', content: [{ type: 'document' }] }] }] }, model(api), context()), /Only text and image tool results/); checks++
}
const chat = convertResponse({ id: 'chat_1', choices: [{ message: { content: 'ok', reasoning_content: 'reason', tool_calls: [{ id: 'call_1', function: { name: 'echo', arguments: '{"v":1}' } }] }, finish_reason: 'tool_calls' }], usage: { prompt_tokens: 20, completion_tokens: 8, prompt_tokens_details: { cached_tokens: 5 } } }, 'm', false, context())
check(chat.content[0].thinking === 'reason' && !chat.content[0].signature && chat.stop_reason === 'tool_use', 'Chat reasoning and tools')
check(chat.usage.input_tokens === 15 && chat.usage.cache_read_input_tokens === 5 && chat.usage.output_tokens === 8, 'Usage cached semantics')
for (const api of ['openai-completions', 'openai-responses'] as const) {
const m = model(api); m.config.supportsReasoning = true; m.config.thinkingLevel = 'auto'
const auto = convertRequest(request, m, context())
check(!auto.reasoning_effort && !auto.reasoning?.effort, `${api}: auto effort is not forwarded`)
m.config.thinkingLevel = 'high'; m.profile.supportedEfforts = ['high']
const high = convertRequest(request, m, context())
check((high.reasoning?.effort ?? high.reasoning_effort) === 'high', `${api}: supported effort forwarded`)
m.config.thinkingLevel = 'low'
assert.throws(() => convertRequest(request, m, context()), /reasoning effort/); checks++
const images = convertRequest({ max_tokens: 30, messages: [{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'img', content: [{ type: 'text', text: 'screenshot' }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'aGk=' } }] }] }] }, model(api), context())
const messages = images.input ?? images.messages
check(messages[0].output === 'screenshot' || messages[0].content === 'screenshot', `${api}: tool text preserved with images`)
check(JSON.stringify(messages[1]).includes('data:image/png;base64,aGk=') && JSON.stringify(messages[1]).includes('img'), `${api}: attributed tool image preserved`)
}
const rc = context(), responseData = { id: 'r1', status: 'completed', output: [{ type: 'reasoning', id: 'rs1', encrypted_content: 'opaque', summary: [{ type: 'summary_text', text: 'reason' }] }, { type: 'function_call', id: 'fc1', call_id: 'call_1', name: 'echo', arguments: '{"v":1}' }], usage: { input_tokens: 7, output_tokens: 4 } }
const converted = convertResponse(responseData, 'm', true, rc)
const replay = convertRequest({ max_tokens: 30, messages: [{ role: 'assistant', content: converted.content }, { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_1', content: 'ok' }] }] }, model('openai-responses'), rc)
check(replay.input[0].encrypted_content === 'opaque' && replay.input[1].call_id === 'call_1', 'Responses opaque reasoning exact-history replay')
assert.throws(() => convertRequest({ max_tokens: 30, messages: [{ role: 'assistant', content: converted.content }] }, model('openai-responses'), context()), /history is not available/); checks++
rc.responsePrefix = undefined
convertResponse({ ...responseData, output: [{ ...responseData.output[0], encrypted_content: 'different' }, responseData.output[1]] }, 'm', true, rc)
assert.throws(() => convertRequest({ max_tokens: 30, messages: [{ role: 'assistant', content: converted.content }] }, model('openai-responses'), rc), /Ambiguous/); checks++
// Identical visible replies at different history occurrences must not share opaque state.
const occurrences = context(), history: any[] = [{ role: 'user', content: 'first' }]
const hiddenReply = (opaque: string) => ({ id: opaque, status: 'completed', output: [{ type: 'reasoning', id: opaque, summary: [], encrypted_content: opaque }, { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Done' }] }] })
convertRequest({ max_tokens: 20, messages: history }, model('openai-responses'), occurrences)
const firstDone = convertResponse(hiddenReply('opaque-first'), 'm', true, occurrences)
history.push({ role: 'assistant', content: firstDone.content }, { role: 'user', content: 'second' })
convertRequest({ max_tokens: 20, messages: history }, model('openai-responses'), occurrences)
const secondDone = convertResponse(hiddenReply('opaque-second'), 'm', true, occurrences)
history.push({ role: 'assistant', content: secondDone.content }, { role: 'user', content: 'third' })
const repeated = convertRequest({ max_tokens: 20, messages: history }, model('openai-responses'), occurrences)
check(repeated.input.filter((x: any) => x.type === 'reasoning').map((x: any) => x.encrypted_content).join(',') === 'opaque-first,opaque-second', 'Identical visible responses replay their own history occurrence')
assert.throws(() => convertRequest({ max_tokens: 20, messages: [{ role: 'assistant', content: firstDone.content }, { role: 'user', content: 'compacted' }] }, model('openai-responses'), occurrences), /compaction or fork/); checks++
// UTF-8/SSE fragmentation, CRLF and multiline data.
const encoded = new TextEncoder().encode('data: {"text":\r\ndata: "你好"}\r\n\r\ndata: [DONE]\n\n')
const parsed = []
for await (const value of readSSE(new ReadableStream({ start(c) { for (const byte of encoded) c.enqueue(new Uint8Array([byte])); c.close() } }))) parsed.push(value)
check((parsed[0] as any).text === '你好' && parsed[1] === '[DONE]', 'SSE fragmented UTF8 and CRLF')
let mode = 'normal', last: any, aborted = false, hits = 0
const upstream = createServer(async (req, res) => {
hits++; const chunks = []; for await (const chunk of req) chunks.push(chunk)
last = { path: req.url, auth: req.headers.authorization, body: JSON.parse(Buffer.concat(chunks).toString()) }
if (mode === 'error') { res.writeHead(401); res.end('mock-upstream-secret'); return }
if (mode === 'redirect') { res.writeHead(307, { location: 'http://127.0.0.1:1/never' }); res.end(); return }
if (mode === 'cancel') { res.writeHead(200, { 'content-type': 'text/event-stream' }); res.write(': waiting\n\n'); res.once('close', () => { aborted = true }); return }
const responses = req.url === '/v1/responses'
const completion = responses ? { id: 'r2', status: 'completed', output: [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: '你好' }] }, { type: 'function_call', call_id: 'call_2', name: 'echo', arguments: '{"v":2}' }], usage: { input_tokens: 9, output_tokens: 3 } } : { id: 'c2', choices: [{ message: { content: '你好', tool_calls: [{ id: 'call_2', function: { name: 'echo', arguments: '{"v":2}' } }] }, finish_reason: 'tool_calls' }], usage: { prompt_tokens: 9, completion_tokens: 3 } }
if (responses && ['hidden', 'reasoning'].includes(mode)) completion.output.push({ type: 'reasoning', id: 'rs2', encrypted_content: 'opaque-private-history', summary: mode === 'hidden' ? [] : [{ type: 'summary_text', text: 'one' }, { type: 'summary_text', text: 'two' }] } as any)
if (!last.body.stream) { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(completion)); return }
res.writeHead(200, { 'content-type': 'text/event-stream' })
const events = responses ? [
{ type: 'response.created', response: { id: 'r2' } },
{ type: 'response.output_item.added', output_index: 0, item: { type: 'message', id: 'm1' } },
{ type: 'response.output_text.delta', output_index: 0, content_index: 0, delta: '你好' },
{ type: 'response.output_text.done', output_index: 0, content_index: 0, text: '你好' },
{ type: 'response.output_item.added', output_index: 1, item: { type: 'function_call', call_id: 'call_2', name: 'echo' } },
{ type: 'response.function_call_arguments.delta', output_index: 1, delta: '{"v":' },
{ type: 'response.function_call_arguments.delta', output_index: 1, delta: '2}' },
{ type: 'response.output_item.done', output_index: 1, item: completion.output[1] },
{ type: 'response.completed', response: completion }
] : [
{ choices: [{ index: 0, delta: { content: '你好' }, finish_reason: null }] },
{ choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: 'call_2', function: { name: 'echo', arguments: '{"v":' } }] }, finish_reason: null }] },
{ choices: [{ index: 0, delta: { tool_calls: [{ index: 0, function: { arguments: '2}' } }] }, finish_reason: 'tool_calls' }] },
{ choices: [], usage: completion.usage }
]
if (responses && mode === 'reasoning') events.splice(events.length - 1, 0,
{ type: 'response.output_item.added', output_index: 2, item: { type: 'reasoning', id: 'rs2' } } as any,
{ type: 'response.reasoning_summary_text.delta', output_index: 2, summary_index: 0, delta: 'one' } as any,
{ type: 'response.reasoning_summary_text.delta', output_index: 2, summary_index: 1, delta: 'two' } as any,
{ type: 'response.output_item.done', output_index: 2, item: completion.output[2] } as any)
if (responses && mode === 'mismatch') (events[2] as any).delta = 'truncated'
const wire = events.map(x => `data: ${JSON.stringify(x)}\r\n\r\n`).join('') + (responses ? '' : 'data: [DONE]\n\n')
const bytes = Buffer.from(mode === 'eof' ? wire.slice(0, wire.indexOf('data:', 10)) : wire)
for (let i = 0; i < bytes.length; i += 7) res.write(bytes.subarray(i, i + 7))
res.end()
})
await new Promise<void>(resolve => upstream.listen(0, '127.0.0.1', resolve))
const upstreamUrl = `http://127.0.0.1:${(upstream.address() as AddressInfo).port}/v1`
try {
for (const api of ['openai-completions', 'openai-responses'] as const) {
const original = model(api, upstreamUrl), lease = await acquireModelBridge(original)
original.config.model = 'changed'; original.config.apiKey = 'changed'
const send = (body: any, path = '/v1/messages', token = lease.apiKey, signal?: AbortSignal) => fetch(lease.baseUrl + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': token }, body: JSON.stringify(body), signal })
try {
for (const stream of [false, true]) {
mode = 'normal'
const result = await send({ ...request, stream })
check(result.status === 200, `${api}: HTTP ${stream ? 'stream' : 'json'}`)
check(last.path === (api === 'openai-responses' ? '/v1/responses' : '/v1/chat/completions') && last.body.model === 'offline-model' && last.auth === 'Bearer mock-upstream-secret', `${api}: frozen route and credentials`)
if (stream) {
const frames: any[] = []; for await (const event of readSSE(result.body!)) frames.push(event)
check(frames[0].type === 'message_start' && frames.at(-1).type === 'message_stop', `${api}: valid stream envelope`)
check(frames.filter(x => x.type === 'content_block_start').length === 2 && frames.filter(x => x.type === 'content_block_stop').length === 2, `${api}: closed text/tool blocks`)
check(frames.find(x => x.type === 'message_delta').usage.input_tokens === 9, `${api}: reported stream usage`)
check(frames.find(x => x.type === 'message_delta').delta.stop_reason === 'tool_use', `${api}: tool stop reason`)
} else {
const data = await result.json(); check(data.content[1].input.v === 2 && data.usage.input_tokens === 9, `${api}: nonstream content`)
}
}
const before = hits
check((await send(request, '/v1/messages', 'wrong')).status === 401, 'Wrong token denied')
check((await send(request, '/v1/other')).status === 404, 'Unknown path denied')
check((await send(request, '/v1/messages?url=http://bad')).status === 404, 'Query injection denied')
check(hits === before, 'Invalid requests never reach upstream')
const count = await send(request, '/v1/messages/count_tokens'), countBody = await count.json()
check(countBody.estimated === true && count.headers.get('x-ycode-token-count-source')?.includes('estimate'), 'Token count explicitly estimated')
mode = 'error'; const error = await send(request); check(error.status === 401 && !(await error.text()).includes('mock-upstream-secret'), 'Upstream errors redacted')
mode = 'redirect'; check((await send(request)).status === 502, 'Redirect not followed')
mode = 'eof'; const eof = await (await send({ ...request, stream: true })).text(); check(eof.includes('event: error') && !eof.includes('event: message_stop'), 'Truncated stream fails explicitly')
mode = 'cancel'; aborted = false
const controller = new AbortController(), pending = await send({ ...request, stream: true }, '/v1/messages', lease.apiKey, controller.signal)
controller.abort(); await pending.body?.cancel().catch(() => {})
for (let i = 0; i < 100 && !aborted; i++) await new Promise(r => setTimeout(r, 10))
check(aborted, `${api}: client disconnect aborts upstream`)
} finally { await Promise.all([lease.close(), lease.close()]) }
await assert.rejects(fetch(lease.baseUrl)); checks++
}
const dir = await mkdtemp(join(tmpdir(), 'ycode-bridge-'))
try {
for (const reasoningMode of ['hidden', 'reasoning']) {
const file = join(dir, reasoningMode + '.json')
const first = await acquireModelBridge(model('openai-responses', upstreamUrl), { historyFile: file })
mode = reasoningMode
let assistant: any[]
try {
const result = await fetch(first.baseUrl + '/v1/messages', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': first.apiKey }, body: JSON.stringify({ ...request, stream: reasoningMode === 'reasoning' }) })
if (reasoningMode === 'reasoning') {
const blocks: any[] = []
for await (const frame of readSSE(result.body!)) {
const f = frame as any
if (f.type === 'content_block_start') blocks[f.index] = { ...f.content_block }
if (f.type === 'content_block_delta') {
const b = blocks[f.index]
if (f.delta.type === 'input_json_delta') b.args = (b.args ?? '') + f.delta.partial_json
else b[b.type] += f.delta[b.type]
}
check(f.type !== 'error', 'Responses multioutput reasoning stream succeeds')
}
assistant = blocks.map(b => { if (b.type === 'tool_use') { b.input = JSON.parse(b.args); delete b.args }; return b })
} else assistant = (await result.json()).content
} finally { await first.close() }
const disk = await readFile(file, 'utf8')
check(disk.includes('opaque-private-history') && !disk.includes('mock-upstream-secret'), 'History persists opaque state without API credentials')
if (process.platform !== 'win32') check(((await stat(file)).mode & 0o777) === 0o600, 'History private file permissions')
const second = await acquireModelBridge(model('openai-responses', upstreamUrl), { historyFile: file })
try {
mode = 'normal'
const result = await fetch(second.baseUrl + '/v1/messages', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': second.apiKey }, body: JSON.stringify({ max_tokens: 50, messages: [...request.messages, { role: 'assistant', content: assistant! }, { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_2', content: 'ok' }] }] }) })
check(result.status === 200 && last.body.input.some((x: any) => x.encrypted_content === 'opaque-private-history'), `${reasoningMode}: close/reacquire replays opaque reasoning`)
await result.body?.cancel()
mode = 'mismatch'
const mismatch = await fetch(second.baseUrl + '/v1/messages', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': second.apiKey }, body: JSON.stringify({ ...request, stream: true }) })
check((await mismatch.text()).includes('event: error'), 'Responses final content mismatch fails')
} finally { await second.close() }
}
const bad = join(dir, 'bad.json'); await writeFile(bad, '{broken')
await assert.rejects(acquireModelBridge(model('openai-responses', upstreamUrl), { historyFile: bad }), /history/); checks++
await writeFile(bad, JSON.stringify({ version: 1, entries: [['[]', [{ type: 'reasoning', summary: 'invalid' }]]] }))
await assert.rejects(createConversionContext(bad), /history/); checks++
const snapshotFile = join(dir, 'snapshot.json'), saved = await createConversionContext(snapshotFile)
convertResponse(responseData, 'm', true, saved)
const saving = saved.persist!(); saved.responsesHistory.clear(); await saving
check((await createConversionContext(snapshotFile)).responsesHistory.size === 1, 'Persistence snapshots before await')
await writeFile(join(dir, 'blocked'), 'not a directory')
const blocked = await createConversionContext(join(dir, 'blocked', 'history.json')).catch(() => null)
check(blocked === null, 'Invalid history path fails closed')
} finally { await rm(dir, { recursive: true, force: true }) }
} finally { upstream.closeAllConnections(); await new Promise<void>(resolve => upstream.close(() => resolve())) }
console.log(`Protocol bridge offline regression: ${checks} checks passed (no SDK/CLI or external API used)`)