Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('enabledAccountModels', () => {
credential: { type: 'auth-token', token: 'lc-test' },
models: [
{ id: 'openai/gpt-5.6', protocols: ['openai-chat', 'openai-responses'] },
{ id: 'anthropic/claude-sonnet-5', protocols: ['openai-chat'] },
{ id: 'anthropic/claude-sonnet-5', protocols: ['openai-chat', 'anthropic'] },
// Probed before `protocols` existed, or never probed — absence must still offer it.
{ id: 'openai/gpt-4.1' },
],
Expand All @@ -83,9 +83,9 @@ describe('enabledAccountModels', () => {
'openai/gpt-5.6',
'openai/gpt-4.1',
]);
// opencode/pi accept any wire this account offers, so nothing here is narrowed.
// opencode falls through to the account's own native anthropic wire (no knownProvider pins it
// elsewhere), so only the model actually tagged for it, plus the untagged one, survive.
Comment thread
xiaoland marked this conversation as resolved.
Comment thread
xiaoland marked this conversation as resolved.
expect(enabledAccountModels([gateway], {}, 'opencode').map(({ model }) => model.id)).toEqual([
'openai/gpt-5.6',
'anthropic/claude-sonnet-5',
'openai/gpt-4.1',
]);
Expand Down
15 changes: 9 additions & 6 deletions packages/foundation/providers/src/__tests__/resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,23 +197,26 @@ describe('resolveBinding: variant chosen per agent', () => {
).toEqual({ tier: 'unavailable', reason: 'protocol-unsupported' });
});

it('serves LinkCode Gateway over both OpenAI wires', () => {
it('serves LinkCode Gateway natively over Anthropic Messages and OpenAI Responses', () => {
const gateway = account({
service: 'linkcode-gateway',
credential: { type: 'auth-token', token: 'lc-gateway-key' },
});
expect(resolveBinding(gateway, 'claude-code')).toEqual({
tier: 'translate',
protocol: 'openai-chat',
baseUrl: 'https://gateway.linkcode.ai/v1',
tier: 'native',
protocol: 'anthropic',
baseUrl: 'https://gateway.linkcode.ai',
});
// Neither wire carries a knownProvider for these two (LinkCode Gateway names no vendor
// either agent's own catalog knows), so they fall through to protocol order like StepFun's
// Pi binding does — now landing on the same native anthropic wire as claude-code.
const providerRoutedKinds = ['opencode', 'pi'] as const;
for (let i = 0, len = providerRoutedKinds.length; i < len; i++) {
const kind = providerRoutedKinds[i];
expect(resolveBinding(gateway, kind)).toEqual({
tier: 'native',
protocol: 'openai-chat',
baseUrl: 'https://gateway.linkcode.ai/v1',
protocol: 'anthropic',
baseUrl: 'https://gateway.linkcode.ai',
});
}
expect(resolveBinding(gateway, 'codex')).toEqual({
Expand Down
9 changes: 9 additions & 0 deletions packages/foundation/providers/src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ export const SERVICE_CATALOG: ServiceDescriptor[] = [
wire: 'openai',
},
},
// Bare origin, not `/v1` — claude-code's own SDK appends `/v1/messages`, matching every
// other `anthropic` variant in this file. Lists only the Claude models served natively.
anthropic: {
Comment thread
xiaoland marked this conversation as resolved.
baseUrl: 'https://gateway.linkcode.ai',
models: {
url: 'https://gateway.linkcode.ai/v1/models?protocol=anthropic',
Comment thread
xiaoland marked this conversation as resolved.
wire: 'anthropic',
},
},
},
models: { url: 'https://gateway.linkcode.ai/v1/models', wire: 'openai' },
},
Expand Down
19 changes: 17 additions & 2 deletions packages/host/engine/src/__tests__/engine-model-probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ describe('config.probe-models', () => {
if (url === '/v1/models?protocol=openai-responses') {
return { status: 200, body: JSON.stringify({ data: [{ id: 'openai/gpt-5.6' }] }) };
}
if (url === '/v1/models?protocol=anthropic') {
Comment thread
xiaoland marked this conversation as resolved.
return {
status: 200,
body: JSON.stringify({ data: [{ id: 'anthropic/claude-sonnet-5' }] }),
};
}
if (url === '/v1/models') {
return {
status: 200,
Expand Down Expand Up @@ -235,11 +241,20 @@ describe('config.probe-models', () => {
// Returned by both lists: tagged with every protocol whose list named it.
protocols: ['openai-chat', 'openai-responses'],
},
{ id: 'anthropic/claude-sonnet-5', protocols: ['openai-chat'] },
{
id: 'anthropic/claude-sonnet-5',
// Protocol order follows PROTOCOL_ORDER's processing order, not the mock's response
// order: the anthropic-only list merges in before the shared openai-chat one.
protocols: ['anthropic', 'openai-chat'],
},
]),
);
expect(reply.models).toHaveLength(2);
expect(seen.sort()).toEqual(['/v1/models', '/v1/models?protocol=openai-responses']);
expect(seen.sort()).toEqual([
'/v1/models',
'/v1/models?protocol=anthropic',
'/v1/models?protocol=openai-responses',
]);
});

it('refuses to send an account secret to a service it does not belong to', async () => {
Expand Down
Loading