feat: complete WorkBuddy parity and Windows release workflow
This commit is contained in:
1 parent
143516b3a8
commit
d5512df549
434 files changed
+54977
-1854
No files matched your search
@@ -0,0 +1,130 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createServer } from 'node:http'
|
||||
import { mkdtemp, readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { once } from 'node:events'
|
||||
import { normalizeModelAddress, endpointCandidates, endpointUrl, modelEndpoint } from '../src/shared/model-endpoint'
|
||||
import { cheapestModelPrice, lookupModelMetadata, type ModelCatalog } from '../src/shared/model-metadata'
|
||||
import { discoverModelConnection, modelFetch } from '../src/main/model-connection'
|
||||
import { Gateway } from '../src/main/gateway'
|
||||
import { buildAnthropicRequest, buildGeminiRequest } from '../src/main/gateway/native-protocols'
|
||||
import { prepareOmpModel, supportsOmpModel } from '../src/main/omp-model'
|
||||
import { OmpKernel, detectOmpPath } from '../src/main/omp'
|
||||
import type { ModelApi, ResolvedModel } from '../src/shared/models'
|
||||
|
||||
const directory = await mkdtemp(resolve('.tmp/custom-model-regression-'))
|
||||
const checks: string[] = []
|
||||
function check(label: string, run: () => void): void { run(); checks.push(label) }
|
||||
const catalog = JSON.parse(await readFile('resources/model-metadata/models.dev.json', 'utf8')) as ModelCatalog
|
||||
const source = JSON.parse(await readFile('resources/model-metadata/manifest.json', 'utf8'))
|
||||
check('domain-only, local origin and full routes normalize', () => {
|
||||
assert.equal(normalizeModelAddress('api.example.com'), 'https://api.example.com')
|
||||
assert.equal(normalizeModelAddress('127.0.0.1:9000'), 'http://127.0.0.1:9000')
|
||||
assert.throws(() => normalizeModelAddress('https://user:password@example.com'))
|
||||
assert.throws(() => normalizeModelAddress('https://example.com?key=secret'))
|
||||
assert(endpointCandidates('https://example.com/proxy', 'anthropic').some(endpoint => endpointUrl(endpoint, 'generate') === 'https://example.com/proxy/v1/messages'))
|
||||
})
|
||||
check('metadata uses the canonical provider and cheapest context tier', () => {
|
||||
const value = lookupModelMetadata(catalog, 'gpt-5.4', 'openai', source)!
|
||||
assert.equal(value.providerId, 'openai'); assert.equal(value.contextWindowTokens, 272000)
|
||||
assert.equal(value.maxInputTokens, 144000); assert(value.supportsImages); assert(value.supportsReasoning)
|
||||
assert.equal(value.price?.input, 2.5); assert.equal(value.price?.output, 15)
|
||||
assert.equal(lookupModelMetadata(catalog, 'openai/gpt-5.4', 'auto', source)?.modelId, 'gpt-5.4')
|
||||
assert.equal(lookupModelMetadata(catalog, 'gpt-5.4-not-a-real-alias', 'auto', source), null)
|
||||
assert.equal(cheapestModelPrice({ id: 'bad', cost: { input: -1, output: 1 } }), undefined)
|
||||
})
|
||||
check('tier thresholds constrain defaults without changing provider', () => {
|
||||
const price = cheapestModelPrice({ id: 'test', limit: { context: 100000 }, cost: { input: 4, output: 8, tiers: [{ input: 1, output: 2, tier: { type: 'context', size: 32000 } }, { input: 5, output: 10, tier: { type: 'context', size: 64000 } }] } })!
|
||||
assert.equal(price.minContext, 32000); assert.equal(price.maxContext, 64000)
|
||||
})
|
||||
|
||||
let api: ModelApi = 'anthropic-messages', anonymous = false
|
||||
const requests: Array<{ api: ModelApi; path: string; body: any }> = []
|
||||
const rejectedAuth: unknown[] = []
|
||||
const anthropic = (model: string): string => [
|
||||
{ type: 'message_start', message: { id: 'msg_test', type: 'message', role: 'assistant', content: [], model, stop_reason: null, stop_sequence: null, usage: { input_tokens: 10, output_tokens: 0 } } },
|
||||
{ type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } },
|
||||
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '协议正常' } },
|
||||
{ type: 'content_block_stop', index: 0 },
|
||||
{ type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 2 } },
|
||||
{ type: 'message_stop' }
|
||||
].map(value => `event: ${value.type}\ndata: ${JSON.stringify(value)}\n\n`).join('')
|
||||
const gemini = (): string => 'data: ' + JSON.stringify({ candidates: [{ index: 0, content: { role: 'model', parts: [{ text: '协议正常' }] }, finishReason: 'STOP' }], usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 2, totalTokenCount: 12 } }) + '\n\n'
|
||||
const server = createServer(async (req, res) => {
|
||||
const url = new URL(req.url!, 'http://local')
|
||||
if (url.pathname === '/redirect') { res.writeHead(302, { location: 'http://127.0.0.1:1/trap' }); res.end(); return }
|
||||
if (req.method === 'GET') {
|
||||
const valid = api === 'anthropic-messages' ? url.pathname === '/v1/models' && req.headers['x-api-key'] === 'controlled-test-key'
|
||||
: api === 'google-generative-ai' ? url.pathname === '/v1beta/models' && req.headers['x-goog-api-key'] === 'controlled-test-key' : url.pathname === '/v1/models' && req.headers.authorization === 'Bearer controlled-test-key'
|
||||
res.writeHead(valid ? 200 : 404, { 'content-type': 'application/json' })
|
||||
res.end(JSON.stringify(api === 'google-generative-ai' ? { models: [{ name: 'models/gemini-2.5-flash', displayName: 'Gemini test', supportedGenerationMethods: ['generateContent'] }, { name: 'models/embed', supportedGenerationMethods: ['embedContent'] }] }
|
||||
: { data: [{ id: 'claude-3-5-haiku-latest' }, { id: 'claude-3-5-haiku-latest' }] })); return
|
||||
}
|
||||
let raw = ''; for await (const chunk of req) raw += chunk
|
||||
const authorized = anonymous ? !req.headers['x-api-key'] && !req.headers['x-goog-api-key'] && !req.headers.authorization
|
||||
: api === 'anthropic-messages' ? req.headers['x-api-key'] === 'controlled-test-key' && !req.headers.authorization : req.headers['x-goog-api-key'] === 'controlled-test-key'
|
||||
if (!authorized) { rejectedAuth.push({ api, anonymous, path: req.url, headers: Object.keys(req.headers).filter(name => /api-key|authorization/.test(name)), googleSentinel: req.headers['x-goog-api-key'] === 'N/A' }); res.writeHead(401, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Incorrect native authentication' } })); return }
|
||||
const body = JSON.parse(raw || '{}'); requests.push({ api, path: req.url!, body })
|
||||
res.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
res.end(api === 'anthropic-messages' ? anthropic(body.model) : gemini())
|
||||
})
|
||||
server.listen(0, '127.0.0.1'); await once(server, 'listening')
|
||||
const address = server.address(); assert(address && typeof address === 'object')
|
||||
const root = `http://127.0.0.1:${address.port}`
|
||||
try {
|
||||
await assert.rejects(() => modelFetch(root + '/redirect', {}), /不同域名/); checks.push('credentials cannot follow a cross-origin redirect')
|
||||
for (const currentApi of ['anthropic-messages', 'google-generative-ai'] as const) {
|
||||
api = currentApi
|
||||
const discovery = await discoverModelConnection(root, 'auto', 'controlled-test-key', true)
|
||||
assert.equal(discovery.endpoint.api, api); assert.equal(discovery.models.length, 1)
|
||||
checks.push(api + ' model discovery negotiates path and auth, filters and deduplicates')
|
||||
const config = { api, baseUrl: discovery.endpoint.baseUrl, model: api === 'anthropic-messages' ? 'claude-3-5-haiku-latest' : 'gemini-2.5-flash', apiKey: 'controlled-test-key', temperature: 0, maxToolRounds: 2, maxOutputTokens: 128 }
|
||||
const events = []
|
||||
for await (const event of new Gateway(config).stream({ messages: [{ role: 'user', content: 'hello' }] })) events.push(event)
|
||||
assert.equal(events.filter(event => event.type === 'text').map(event => event.text).join(''), '协议正常')
|
||||
assert(events.some(event => event.type === 'usage' && event.usage.totalTokens === 12))
|
||||
checks.push(api + ' fallback gateway streams native text and usage')
|
||||
const body = api === 'anthropic-messages' ? buildAnthropicRequest(config, { messages: [{ role: 'user', content: [{ type: 'image_url', image_url: { url: 'data:image/png;base64,AAAA' } }] }] })
|
||||
: buildGeminiRequest(config, { messages: [{ role: 'user', content: [{ type: 'image_url', image_url: { url: 'data:image/png;base64,AAAA' } }] }] })
|
||||
assert(JSON.stringify(body).includes('AAAA')); checks.push(api + ' image input preserves base64 data')
|
||||
|
||||
const executable = detectOmpPath(); assert(executable, 'Real OMP required for native protocol acceptance')
|
||||
for (const authMode of ['api-key', 'none'] as const) {
|
||||
anonymous = authMode === 'none'
|
||||
const resolved: ResolvedModel = { profile: { id: config.model, providerId: 'custom', name: config.model, model: config.model, baseUrl: root, protocol: api === 'anthropic-messages' ? 'anthropic' : 'gemini', authMode, hasApiKey: !anonymous, keyStorage: 'none', supportsReasoning: true, supportsImages: true }, config: { ...config, authMode, apiKey: anonymous ? '' : config.apiKey } }
|
||||
if (!supportsOmpModel(resolved)) {
|
||||
assert.equal(api, 'google-generative-ai'); assert.equal(authMode, 'none')
|
||||
await assert.rejects(() => prepareOmpModel(resolved), /原生 HTTP/)
|
||||
checks.push('keyless Gemini explicitly routes to the native HTTP gateway rather than sending OMP sentinel credentials')
|
||||
continue
|
||||
}
|
||||
process.env.YCODE_OMP_HOME = directory
|
||||
const managed = await prepareOmpModel(resolved), frames: string[] = [], errors: string[] = []
|
||||
assert(!(await readFile(resolve(directory, '.omp/profiles', managed.profile, 'agent/models.yml'), 'utf8')).includes(config.apiKey), 'native profile must never store the API key')
|
||||
const kernel = new OmpKernel(executable, directory, managed.profile, undefined, managed.model, managed.env)
|
||||
kernel.on('error', error => errors.push(String(error)))
|
||||
kernel.on('stderr', value => { if (/error/i.test(String(value))) errors.push(String(value).slice(0, 300)) })
|
||||
const start = requests.length
|
||||
try {
|
||||
await kernel.start()
|
||||
const auto = await kernel.send({ type: 'set_thinking_level', level: 'auto' })
|
||||
assert.equal(auto.success, true, 'OMP must accept automatic thinking intensity')
|
||||
await kernel.setThinkingLevel('minimal')
|
||||
const end = new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(Error('Native protocol turn timeout: ' + JSON.stringify({ api, paths: requests.slice(start).map(r => r.path), frames, errors }))), 18000)
|
||||
kernel.on('event', frame => { frames.push(frame.type); if (frame.type === 'agent_end') { clearTimeout(timer); resolve() } })
|
||||
})
|
||||
await kernel.prompt('Reply briefly.'); await end
|
||||
assert(requests.length > start, JSON.stringify({ api, authMode, frames, errors, rejectedAuth }))
|
||||
const request = requests[start]
|
||||
console.log(JSON.stringify({ api, nativePath: request.path, thinking: request.body.thinking ?? request.body.generationConfig?.thinkingConfig, expectedRoot: discovery.endpoint.baseUrl, frameTypes: [...new Set(frames)], errors }))
|
||||
const nativeUrl = new URL(request.path, root)
|
||||
assert.equal(nativeUrl.pathname, api === 'anthropic-messages' ? '/v1/messages' : '/v1beta/models/gemini-2.5-flash:streamGenerateContent')
|
||||
if (api === 'google-generative-ai') assert.equal(nativeUrl.searchParams.get('alt'), 'sse')
|
||||
checks.push(api + ' real OMP native adapter completes using the negotiated route and ' + authMode + ' authentication')
|
||||
} finally { await kernel.dispose() }
|
||||
}
|
||||
anonymous = false
|
||||
}
|
||||
console.log('CUSTOM_MODEL_REGRESSION_PASS ' + JSON.stringify({ directory, checks }))
|
||||
} finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())) }
|
||||
Reference in new issue
Block a user