Skip to content
Open
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
81 changes: 55 additions & 26 deletions packages/runtime/src/__tests__/provider-contract-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,31 @@ async function runGitHubCopilotDiscovery(): Promise<void> {
copilotModel('gpt-5.4', ['/responses']),
copilotModel('claude-sonnet-4.6', ['/v1/messages']),
copilotModel('gemini-3.1-pro-preview', ['/chat/completions']),
{
id: 'policy-free',
name: 'policy-free display',
model_picker_enabled: true,
supported_endpoints: ['/chat/completions'],
capabilities: {
limits: {
max_prompt_tokens: 400_000,
max_output_tokens: 128_000,
},
supports: {
tool_calls: true,
vision: true,
reasoning_effort: ['low', 'medium', 'high'],
},
},
},
{
...copilotModel('disabled-by-policy', ['/chat/completions']),
policy: { state: 'disabled' },
},
{
...copilotModel('not-configured', ['/chat/completions']),
policy: { state: 'unconfigured' },
},
{
...copilotModel('hidden-from-picker', ['/chat/completions']),
model_picker_enabled: false,
Expand Down Expand Up @@ -383,32 +404,40 @@ async function runGitHubCopilotDiscovery(): Promise<void> {
);

assert.deepEqual(models, [
{
id: 'gpt-5.4',
displayName: 'gpt-5.4 display',
contextWindow: 400_000,
maxOutputTokens: 128_000,
apiProtocol: 'openai-responses',
capabilities: { vision: true, reasoning: true, functionCalling: true },
},
{
id: 'claude-sonnet-4.6',
displayName: 'claude-sonnet-4.6 display',
contextWindow: 400_000,
maxOutputTokens: 128_000,
apiProtocol: 'anthropic-messages',
capabilities: { vision: true, reasoning: true, functionCalling: true },
},
{
id: 'gemini-3.1-pro-preview',
displayName: 'gemini-3.1-pro-preview display',
contextWindow: 400_000,
maxOutputTokens: 128_000,
apiProtocol: 'openai-chat',
capabilities: { vision: true, reasoning: true, functionCalling: true },
},
]);
}
{
id: 'gpt-5.4',
displayName: 'gpt-5.4 display',
contextWindow: 400_000,
maxOutputTokens: 128_000,
apiProtocol: 'openai-responses',
capabilities: { vision: true, reasoning: true, functionCalling: true },
},
{
id: 'claude-sonnet-4.6',
displayName: 'claude-sonnet-4.6 display',
contextWindow: 400_000,
maxOutputTokens: 128_000,
apiProtocol: 'anthropic-messages',
capabilities: { vision: true, reasoning: true, functionCalling: true },
},
{
id: 'gemini-3.1-pro-preview',
displayName: 'gemini-3.1-pro-preview display',
contextWindow: 400_000,
maxOutputTokens: 128_000,
apiProtocol: 'openai-chat',
capabilities: { vision: true, reasoning: true, functionCalling: true },
},
{
id: 'policy-free',
displayName: 'policy-free display',
contextWindow: 400_000,
maxOutputTokens: 128_000,
apiProtocol: 'openai-chat',
capabilities: { vision: true, reasoning: true, functionCalling: true },
},
]);
}

function copilotModel(id: string, supportedEndpoints: string[]): Record<string, unknown> {
return {
Expand Down
4 changes: 3 additions & 1 deletion packages/runtime/src/model-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,11 +519,13 @@ function contextWindowOfOpenAiCodexModel(model: RawOpenAiCodexModel): number | u
}

function toGitHubCopilotModelInfo(model: RawGitHubCopilotModel): ModelInfo[] {
if (model.policy !== undefined) {
if (model.policy.state !== 'enabled') return [];
}
if (
typeof model.id !== 'string' ||
!model.id ||
model.model_picker_enabled !== true ||
model.policy?.state === 'disabled' ||
model.capabilities?.supports?.tool_calls !== true
)
return [];
Expand Down
80 changes: 80 additions & 0 deletions packages/storage/src/__tests__/runtime-policy-model-facts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { RuntimePolicyCoordinator } from '../runtime-policy/coordinator.js';
import { ConnectionCatalogDocumentOwner } from '../runtime-policy/connection-catalog-document.js';

test('runtime policy catalog overlays enabled custom model facts without changing the raw catalog', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-runtime-facts-'));
Expand Down Expand Up @@ -259,6 +260,85 @@ test('model fetch keeps enabled facts-backed models when provider inventory fill
}
});

test('github copilot model fetch prunes fallback ids outside the live catalog', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-runtime-facts-copilot-refresh-'));
try {
const catalog = new ConnectionCatalogDocumentOwner();
const connectionId = '00000000-0000-4000-8000-00000000c0de';
const current = {
schemaVersion: 1 as const,
revision: 1,
defaultTarget: { connectionId, modelId: 'copilot-fallback' },
connections: [
{
connectionId,
revision: 1,
slug: 'github-copilot',
name: 'GitHub Copilot',
providerType: 'github-copilot' as const,
enabled: true,
enabledModelIds: ['copilot-fallback'],
models: [{ id: 'copilot-fallback' }],
modelSource: 'fallback' as const,
},
],
};

const refreshed = await catalog.writeModelFetchResult(
root,
current,
{ connectionId, revision: 1 },
{ models: [{ id: 'live-model' }], source: 'fetched', fetchedAt: 1 },
);

const projected = refreshed.connections[0];
assert.deepEqual(projected?.enabledModelIds, []);
assert.deepEqual(projected?.models.map((model) => model.id), ['live-model']);
assert.equal(refreshed.defaultTarget, null);
} finally {
await rm(root, { recursive: true, force: true });
}
});

test('github copilot model fetch clears a withdrawn default without picking a replacement', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-runtime-facts-copilot-default-'));
try {
const catalog = new ConnectionCatalogDocumentOwner();
const connectionId = '00000000-0000-4000-8000-00000000c0df';
const current = {
schemaVersion: 1 as const,
revision: 1,
defaultTarget: { connectionId, modelId: 'copilot-fallback' },
connections: [
{
connectionId,
revision: 1,
slug: 'github-copilot',
name: 'GitHub Copilot',
providerType: 'github-copilot' as const,
enabled: true,
enabledModelIds: ['copilot-fallback', 'retained-live'],
models: [{ id: 'copilot-fallback' }, { id: 'retained-live' }],
modelSource: 'fallback' as const,
},
],
};

const refreshed = await catalog.writeModelFetchResult(
root,
current,
{ connectionId, revision: 1 },
{ models: [{ id: 'retained-live' }], source: 'fetched', fetchedAt: 1 },
);

const projected = refreshed.connections[0];
assert.deepEqual(projected?.enabledModelIds, ['retained-live']);
assert.equal(refreshed.defaultTarget, null);
} finally {
await rm(root, { recursive: true, force: true });
}
});

test('protocol model facts edits clear verification, supersede tickets, and warn on malformed input', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-runtime-facts-external-edit-'));
const emitWarning = process.emitWarning;
Expand Down
27 changes: 23 additions & 4 deletions packages/storage/src/runtime-policy/connection-catalog-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,32 @@ export class ConnectionCatalogDocumentOwner {
aliases: modelIdAliasesForProvider(previous.providerType),
},
);
// GitHub Copilot 的 `/models` 是账户当前可用目录,刷新时不能把旧
// fallback 选择再次保留下来,否则选择器会回流不可用模型。
const isGitHubCopilot = previous.providerType === 'github-copilot';
const liveModelIds = isGitHubCopilot ? new Set(result.models.map(({ id }) => id)) : undefined;
const enabledModelIds = isGitHubCopilot
? reconciled.enabledModelIds.filter((modelId) => liveModelIds?.has(modelId))
: reconciled.enabledModelIds;
const defaultModel =
isGitHubCopilot && liveModelIds && !liveModelIds.has(reconciled.defaultModel)
? ''
: reconciled.defaultModel;
// 目录里没有的 default 只应被清掉,不应顺手换成另一个 live 模型。
// Copilot 的可用集是账户当前事实,不是可自动补位的偏好列表。
// Discovery MOVES a target: a provider's model rename carries the default
// across by alias. A default outside the selection the reconciler just
// decided is its own bug — fail closed where it is still attributable.
const defaultTarget = currentDefaultTarget
? { connectionId: previous.connectionId, modelId: reconciled.defaultModel }
? defaultModel
? { connectionId: previous.connectionId, modelId: defaultModel }
: null
: current.defaultTarget;
if (currentDefaultTarget && !reconciled.enabledModelIds.includes(reconciled.defaultModel)) {
if (
currentDefaultTarget &&
!isGitHubCopilot &&
!reconciled.enabledModelIds.includes(reconciled.defaultModel)
) {
throw codecError(
'invalid_document',
'Model discovery reconciled a default outside its own selection',
Expand All @@ -489,14 +508,14 @@ export class ConnectionCatalogDocumentOwner {
// next read.
const relayModelProfiles = pruneRelayModelProfiles(
previous.relayModelProfiles,
reconciled.enabledModelIds,
enabledModelIds,
);
const { relayModelProfiles: _staleProfiles, ...previousWithoutProfiles } = previous;
const discovered: ConnectionCatalogEntry = {
...previousWithoutProfiles,
...(relayModelProfiles ? { relayModelProfiles } : {}),
revision: nextRevision(previous.revision),
enabledModelIds: reconciled.enabledModelIds,
enabledModelIds,
models: result.models,
modelSource: result.source,
modelsFetchedAt: result.fetchedAt,
Expand Down
Loading