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
@@ -7,6 +7,7 @@ import { pathToFileURL } from 'node:url'
|
||||
import { createServer } from 'node:http'
|
||||
import type { CustomModelInput } from '../src/shared/models'
|
||||
import type * as Registry from '../src/main/models'
|
||||
import { endpointCandidates, inferModelApi } from '../src/shared/model-endpoint'
|
||||
|
||||
const STATE_KEY = '__YCODE_MODEL_REGISTRY_TEST__'
|
||||
const globals = globalThis as typeof globalThis & Record<string, unknown>
|
||||
@@ -32,6 +33,7 @@ try {
|
||||
const legacyBefore = await readFile(configFile, 'utf8')
|
||||
await build({
|
||||
entryPoints: [join(process.cwd(), 'src/main/models.ts')], outfile, bundle: true, platform: 'node', format: 'esm', logLevel: 'silent',
|
||||
define: { 'import.meta.url': JSON.stringify(pathToFileURL(join(process.cwd(), 'src/main/model-metadata.ts')).href) },
|
||||
plugins: [{ name: 'model-registry-electron', setup(pluginBuild) {
|
||||
pluginBuild.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'mock' }))
|
||||
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock' }, () => ({ loader: 'js', contents: `
|
||||
@@ -81,7 +83,8 @@ try {
|
||||
assert.equal(edited?.config.model, 'updated-model')
|
||||
assert.equal(edited?.config.apiKey, 'new-keywith-space', 'editing an empty secret keeps the stored secret')
|
||||
assert.deepEqual(edited?.profile.supportedEfforts, ['high', 'low'])
|
||||
assert.equal(edited?.profile.maxInputTokens, 131072)
|
||||
assert.equal(edited?.profile.contextWindowTokens, 131072)
|
||||
assert.equal(edited?.profile.maxInputTokens, 98304)
|
||||
registry = await reload()
|
||||
assert.equal((await registry.resolveModel())?.profile.id, 'renamed', 'selection survives a fresh module/application load')
|
||||
await registry.setSelectedModel(null)
|
||||
@@ -107,6 +110,16 @@ try {
|
||||
assert.equal(JSON.stringify(fallback).includes('new-keywith-space'), false)
|
||||
state.encryption = true
|
||||
|
||||
for (const protocol of ['openai', 'auto'] as const) {
|
||||
assert.equal(inferModelApi('https://provider.example', protocol), 'openai-responses')
|
||||
const candidates = endpointCandidates('https://provider.example', protocol)
|
||||
assert.equal(candidates[0].api, 'openai-responses')
|
||||
assert(candidates.findIndex(endpoint => endpoint.api === 'openai-completions') > candidates.findLastIndex(endpoint => endpoint.api === 'openai-responses'))
|
||||
assert.equal(endpointCandidates('https://provider.example/v1/chat/completions/', protocol)[0].api, 'openai-completions')
|
||||
}
|
||||
assert.equal(inferModelApi('https://api.anthropic.com', 'auto'), 'anthropic-messages')
|
||||
assert.equal(inferModelApi('https://generativelanguage.googleapis.com', 'auto'), 'google-generative-ai')
|
||||
|
||||
let responseMode = 'success'
|
||||
let responsePayload: unknown = undefined
|
||||
const requests: Array<{ path: string; auth?: string; data: Record<string, unknown> }> = []
|
||||
@@ -120,6 +133,15 @@ try {
|
||||
if (responseMode === 'not-found') { response.statusCode = 404; response.end(JSON.stringify({ error: 'private-endpoint-echo' })); return }
|
||||
if (responseMode === 'malformed') { response.end('not JSON'); return }
|
||||
if (responseMode === 'wrong-shape') { response.end('{}'); return }
|
||||
if (responseMode === 'negotiate-both' || responseMode === 'negotiate-chat-only') {
|
||||
if (request.url?.endsWith('/responses') && responseMode === 'negotiate-both') {
|
||||
response.end(JSON.stringify({ object: 'response', status: 'completed', output: [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'OK' }] }] })); return
|
||||
}
|
||||
if (request.url?.endsWith('/chat/completions')) {
|
||||
response.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: 'OK' } }] })); return
|
||||
}
|
||||
response.statusCode = 404; response.end('{}'); return
|
||||
}
|
||||
if (responsePayload !== undefined) { response.end(JSON.stringify(responsePayload)); return }
|
||||
response.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: 'OK' } }] }))
|
||||
})
|
||||
@@ -201,6 +223,31 @@ try {
|
||||
assert.equal(invalidResult.message.includes(endpoint), false)
|
||||
}
|
||||
responsePayload = undefined
|
||||
responseMode = 'negotiate-both'
|
||||
for (const protocol of ['openai', 'auto'] as const) {
|
||||
const start = requests.length
|
||||
const negotiated = await registry.testModel(input('prefer-responses-' + protocol, { baseUrl: endpoint, protocol }))
|
||||
assert.equal(negotiated.success, true)
|
||||
assert.equal(negotiated.endpoint?.api, 'openai-responses', 'Responses wins when both OpenAI APIs are supported')
|
||||
assert.deepEqual(requests.slice(start).map(request => request.path), ['/v1/responses'], 'a successful Responses probe must not call Chat')
|
||||
}
|
||||
const previousChat = { api: 'openai-completions' as const, baseUrl: endpoint + '/custom', checkedAt: Date.now() - 1000, verifiedBy: 'generation' as const }
|
||||
const previousInput = input('previous-chat', { baseUrl: endpoint, protocol: 'openai', endpoint: previousChat })
|
||||
const startRetest = requests.length
|
||||
const retested = await registry.testModel(previousInput)
|
||||
assert.equal(retested.endpoint?.api, 'openai-responses', 're-testing a stored Chat route follows the new priority')
|
||||
assert.equal(retested.endpoint?.baseUrl, previousChat.baseUrl, 're-testing preserves a negotiated custom root')
|
||||
assert.deepEqual(requests.slice(startRetest).map(request => request.path), ['/v1/custom/responses'])
|
||||
assert.equal(previousChat.api, 'openai-completions', 'testing must not mutate the supplied stored route')
|
||||
const explicitChat = await registry.testModel(input('explicit-chat-protocol', { baseUrl: endpoint + '/chat/completions', protocol: 'openai' }))
|
||||
assert.equal(explicitChat.endpoint?.api, 'openai-completions', 'an explicit Chat URL remains an intentional override')
|
||||
assert.equal(requests.at(-1)?.path, '/v1/chat/completions')
|
||||
responseMode = 'negotiate-chat-only'
|
||||
const startFallback = requests.length
|
||||
const chatFallback = await registry.testModel(input('chat-only-protocol', { baseUrl: endpoint, protocol: 'openai' }))
|
||||
assert.equal(chatFallback.success, true)
|
||||
assert.equal(chatFallback.endpoint?.api, 'openai-completions')
|
||||
assert.deepEqual(requests.slice(startFallback).map(request => request.path), ['/v1/responses', '/v1/chat/completions'], 'unsupported Responses falls back to Chat')
|
||||
responseMode = 'auth'
|
||||
const authFailure = await registry.testModel(testInput)
|
||||
assert.equal(authFailure.success, false)
|
||||
@@ -236,6 +283,42 @@ try {
|
||||
try {
|
||||
assert.match((await registry.testModel(testInput)).message, /超时/)
|
||||
} finally { globalThis.fetch = originalFetch }
|
||||
const largeModel = input('large-sol', { model: 'gpt-6.1-sol', baseUrl: endpoint, protocol: 'openai',
|
||||
endpoint: { api: 'openai-responses', baseUrl: endpoint, checkedAt: Date.now(), verifiedBy: 'generation' },
|
||||
maxInputTokens: 500000, maxOutputTokens: 128000, contextWindowTokens: 272000 })
|
||||
await registry.saveModel(largeModel)
|
||||
let large = await registry.resolveModel(largeModel.id)
|
||||
assert.deepEqual([large?.profile.maxInputTokens, large?.profile.maxOutputTokens, large?.profile.contextWindowTokens], [372000, 128000, 500000])
|
||||
assert.equal(large?.profile.tokenBudgetVersion, 2)
|
||||
assert.deepEqual([large?.profile.price?.input, large?.profile.price?.output, large?.config.maxOutputTokens], [4, 15, 128000])
|
||||
const beforeInvalidLimit = await readFile(modelsFile, 'utf8')
|
||||
await assert.rejects(() => registry.saveModel({ ...largeModel, previousId: largeModel.id, maxOutputTokens: 131072 }), /128,000/)
|
||||
await assert.rejects(() => registry.saveModel({ ...largeModel, previousId: largeModel.id, maxInputTokens: 1100000 }), /1,050,000/)
|
||||
await assert.rejects(() => registry.saveModel({ ...largeModel, previousId: largeModel.id, tokenBudgetVersion: 2, contextWindowTokens: 128000 }), /必须小于/)
|
||||
assert.equal(await readFile(modelsFile, 'utf8'), beforeInvalidLimit, 'invalid token budgets must not change saved profiles')
|
||||
registry = await reload()
|
||||
large = await registry.resolveModel(largeModel.id)
|
||||
assert.equal(large?.profile.contextWindowTokens, 500000, 'configured total survives a fresh registry load')
|
||||
assert.equal(large?.config.maxOutputTokens, 128000, 'the actual request budget survives a fresh registry load')
|
||||
const legacyRegistry = JSON.parse(await readFile(modelsFile, 'utf8'))
|
||||
const legacyBudget = legacyRegistry.models.find((model: {id: string}) => model.id === largeModel.id)
|
||||
delete legacyBudget.tokenBudgetVersion
|
||||
legacyBudget.maxInputTokens = 500000; legacyBudget.contextWindowTokens = 628000
|
||||
await writeFile(modelsFile, JSON.stringify(legacyRegistry))
|
||||
const legacyBytes = await readFile(modelsFile, 'utf8')
|
||||
registry = await reload()
|
||||
const migratedBudget = (await registry.listModels()).models.find(model => model.id === largeModel.id)!
|
||||
assert.deepEqual([migratedBudget.contextWindowTokens, migratedBudget.maxInputTokens, migratedBudget.maxOutputTokens, migratedBudget.tokenBudgetVersion], [500000, 372000, 128000, 2])
|
||||
assert.equal((await registry.resolveModel(largeModel.id))?.profile.contextWindowTokens, 500000)
|
||||
assert.equal(await readFile(modelsFile, 'utf8'), legacyBytes, 'reading legacy budgets must not rewrite credentials or unrelated registry entries')
|
||||
await registry.saveModel({ ...largeModel, previousId: largeModel.id, tokenBudgetVersion: 2, contextWindowTokens: 500000, maxInputTokens: 372000 })
|
||||
registry = await reload()
|
||||
assert.equal((await registry.resolveModel(largeModel.id))?.profile.contextWindowTokens, 500000, 'a persisted migrated budget must not subtract output twice')
|
||||
await registry.saveModel({ ...largeModel, previousId: largeModel.id, maxInputTokens: 32768, maxOutputTokens: 4096, contextWindowTokens: 628000 })
|
||||
const reduced = await registry.resolveModel(largeModel.id)
|
||||
assert.equal(reduced?.profile.contextWindowTokens, 32768)
|
||||
assert.equal(reduced?.profile.maxInputTokens, 28672)
|
||||
assert.equal(reduced?.profile.price?.input, 2)
|
||||
await registry.setSelectedModel('renamed')
|
||||
const deleted = await registry.deleteModel('renamed')
|
||||
assert.equal(deleted.selectedModelId, deleted.models[0].id)
|
||||
@@ -250,7 +333,7 @@ try {
|
||||
registry = await reload()
|
||||
await assert.rejects(() => registry.listModels(), /损坏/)
|
||||
assert.equal(await readFile(modelsFile, 'utf8'), '{malformed', 'corrupt registries must never be silently overwritten')
|
||||
console.log('PASS model registry: migration, encrypted CRUD, concurrent writes, persistence, selection, Chat/Responses HTTP schema, incomplete reasoning, blank-secret reuse and secret-safe failures')
|
||||
console.log('PASS model registry: migration, encrypted CRUD, concurrent writes, persistence, selection, total/output budgets, legacy budget migration, derived input/reload/reduction, catalog limits, Responses priority, explicit Chat override, Chat fallback, stored-route retesting, Chat/Responses HTTP schema, incomplete reasoning, blank-secret reuse and secret-safe failures')
|
||||
} finally {
|
||||
if (server) await new Promise<void>(resolve => server!.close(() => resolve()))
|
||||
globals[STATE_KEY] = previous
|
||||
|
||||
Reference in new issue
Block a user