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
62 changes: 62 additions & 0 deletions packages/runtime/src/__tests__/provider-conformance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,68 @@ describe('models.dev provider conformance', () => {
assert.equal(probedPath, '/v1/responses');
});

test('a Responses relay configured with its host root discovers and probes one /v1 base', async () => {
const requests: Array<{ method: string; url: string }> = [];
const server = await startJsonServer((request, response) => {
requests.push({ method: request.method ?? '', url: request.url ?? '' });
if (request.method === 'GET' && request.url === '/v1/models') {
respondJson(response, 200, { data: [{ id: 'relay-reasoner' }] });
return;
}
if (request.method === 'POST' && request.url === '/v1/responses') {
respondJson(response, 200, {});
return;
}
// A mainstream relay mounts the OpenAI surface under `/v1` only. That is
// what let the host-root form pass the probe and 404 on discovery: both
// addresses have to come from one normalized API base (#3320).
respondJson(response, 404, { error: { message: 'not found' } });
});
const connection: LlmConnection = {
slug: 'responses-relay',
name: 'Responses Relay',
providerType: 'openai-responses-compatible',
baseUrl: server.url,
defaultModel: 'relay-reasoner',
enabled: true,
createdAt: 1,
updatedAt: 1,
};

assert.deepEqual(
(await fetchProviderModels(connection, 'relay-key')).map(({ id }) => id),
['relay-reasoner'],
);
assert.equal((await testConnection(connection, 'relay-key')).ok, true);
assert.deepEqual(requests, [
{ method: 'GET', url: '/v1/models' },
{ method: 'POST', url: '/v1/responses' },
]);
});

test('a DeepSeek root base keeps serving Responses at the root it publishes', async () => {
let probedPath: string | undefined;
const server = await startJsonServer((request, response) => {
probedPath = request.url;
respondJson(response, 200, {});
});
const connection: LlmConnection = {
slug: 'deepseek',
name: 'DeepSeek',
providerType: 'deepseek',
baseUrl: server.url,
defaultModel: 'deepseek-v4-pro',
enabled: true,
createdAt: 1,
updatedAt: 1,
};

// The relay self-heal is provider-scoped: a built-in that publishes an
// unversioned root must not be rewritten to `/v1` behind the user's back.
assert.equal((await testConnection(connection, 'deepseek-token')).ok, true);
assert.equal(probedPath, '/responses');
});

test('Ollama Cloud requests usage in streamed chat completions', async () => {
let requestBody: Record<string, unknown> | undefined;
const server = await startJsonServer(async (request, response) => {
Expand Down
13 changes: 10 additions & 3 deletions packages/runtime/src/__tests__/provider-contract-matrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ function assertDiscoveryRequest(
// Path: the declared path, or the protocol's default models path.
assert.equal(
url.pathname,
expectedDiscoveryPathname(discovery),
expectedDiscoveryPathname(row, discovery),
`${where} must request the declared models path`,
);

Expand Down Expand Up @@ -249,15 +249,22 @@ function assertDiscoveryRequest(
}
}

function expectedDiscoveryPathname(discovery: ProviderContractDiscoveryPlan): string {
function expectedDiscoveryPathname(
row: ProviderContractRow,
discovery: ProviderContractDiscoveryPlan,
): string {
switch (discovery.protocol) {
case 'anthropic':
return '/v1/models';
case 'google':
return '/v1beta/models';
default: {
const path = discovery.path ?? '/models';
return path.startsWith('/') ? path : `/${path}`;
const pathname = path.startsWith('/') ? path : `/${path}`;
// A Responses relay normalizes the host root `baseConnection` hands it to
// `/v1`, so discovery and the Responses request resolve from one API base
// instead of splitting across `/models` and `/v1/models` (#3320).
return row.providerType === 'openai-responses-compatible' ? `/v1${pathname}` : pathname;
}
}
}
Expand Down
68 changes: 67 additions & 1 deletion packages/runtime/src/__tests__/responses-wire-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ import { buildProviderOptions, getAIModel } from '../model-factory.js';
import { resolveModelRuntime } from '../model-runtime.js';
import { lowerModelTools } from '../model-adapter.js';
import { openAiCodexCompactionMessages } from '../openai-codex-history-compactor.js';
import { openAiResponsesBaseUrl, openResponsesUrl } from '../provider-urls.js';
import {
openAiResponsesBaseUrl,
openResponsesUrl,
responsesRelayApiBaseUrl,
} from '../provider-urls.js';

function conn(providerType: LlmConnection['providerType'], slug = 'test'): LlmConnection {
return {
Expand Down Expand Up @@ -141,6 +145,68 @@ describe('responses wire contract', () => {
assert.deepEqual(urls, ['https://relay.example/v1/responses']);
});

test('resolves a Responses relay host root to one versioned API base', async () => {
assert.equal(
responsesRelayApiBaseUrl('http://relay.example:3000'),
'http://relay.example:3000/v1',
);
assert.equal(
responsesRelayApiBaseUrl('http://relay.example:3000/'),
'http://relay.example:3000/v1',
);
assert.equal(responsesRelayApiBaseUrl('https://relay.example/v1'), 'https://relay.example/v1');
assert.equal(responsesRelayApiBaseUrl('https://relay.example/v1/'), 'https://relay.example/v1');
assert.equal(
responsesRelayApiBaseUrl('https://relay.example/v1/responses'),
'https://relay.example/v1',
);
assert.equal(
responsesRelayApiBaseUrl('https://relay.example/relay/v1'),
'https://relay.example/relay/v1',
);
// No base to normalize: the caller fails where it already failed instead of
// on a URL constructor.
assert.equal(responsesRelayApiBaseUrl(''), '');

// The relay resolves send, probe, and discovery from that one base, while a
// built-in that publishes an unversioned root keeps it (#3320).
assert.equal(
resolveModelRuntime(
{ providerType: 'openai-responses-compatible', baseUrl: 'http://relay.example:3000' },
'relay-model',
).baseUrl,
'http://relay.example:3000/v1',
);
assert.equal(
resolveModelRuntime({ providerType: 'deepseek' }, 'deepseek-v4-flash').baseUrl,
'https://api.deepseek.com',
);

const urls: string[] = [];
const fetch = (async (url: string | URL | Request) => {
urls.push(String(url));
return Response.json({
id: 'r',
object: 'response',
status: 'completed',
output: [],
usage: { input_tokens: 1, output_tokens: 1 },
});
}) as typeof globalThis.fetch;
const model = getAIModel({
connection: { ...conn('openai-responses-compatible'), baseUrl: 'http://relay.example:3000' },
apiKey: '[redacted]',
modelId: 'relay-model',
fetch,
});

await model.doGenerate({
prompt: [{ role: 'user', content: [{ type: 'text', text: 'ping' }] }],
});

assert.deepEqual(urls, ['http://relay.example:3000/v1/responses']);
});

test('resolves only supported Responses adapter and replay pairings', () => {
const deepseek = resolveModelRuntime({ providerType: 'deepseek' }, 'deepseek-v4-flash');
assert.deepEqual(deepseek.reasoningReplay, {
Expand Down
12 changes: 10 additions & 2 deletions packages/runtime/src/model-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
CONNECTION_MODEL_ID_MAX_LENGTH,
normalizeConnectionModelDiscoveryResult,
} from '@maka/core/runtime-policy';
import { anthropicV1Url, googleApiUrl } from './provider-urls.js';
import { anthropicV1Url, googleApiUrl, responsesRelayApiBaseUrl } from './provider-urls.js';
import { openAiCodexHeaders } from './subscription-auth.js';
import {
GITHUB_COPILOT_API_VERSION,
Expand Down Expand Up @@ -179,7 +179,15 @@ async function fetchProviderModelsStrict(
apiKey: string,
fetchFn: ConnectionEffectFetch | undefined,
): Promise<ModelInfo[]> {
const baseUrl = effectiveBaseUrl(connection);
// A Responses relay serves `/models` next to `/responses`, so discovery has
// to resolve from the same API base `resolveModelRuntime` hands the send and
// probe paths — otherwise a configured host root passes the probe and 404s
// here (#3320).
const configuredBaseUrl = effectiveBaseUrl(connection);
const baseUrl =
connection.providerType === 'openai-responses-compatible'
? responsesRelayApiBaseUrl(configuredBaseUrl)
: configuredBaseUrl;
const definition = PROVIDER_DEFAULTS[connection.providerType];
// Unknown providerType → no discovery path. Throw a clear error (caught and
// generalized by the caller) rather than crashing on `.modelDiscovery`.
Expand Down
9 changes: 8 additions & 1 deletion packages/runtime/src/model-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
openAiAdapterApiProtocol,
} from '@maka/core/model-metadata';
import { isRetiredProvider } from '@maka/core/provider-registry';
import { responsesRelayApiBaseUrl } from './provider-urls.js';
import { resolveApplyPatchProfile, type ApplyPatchProfile } from './apply-patch-profile.js';

export type ModelRuntimeWire =
Expand Down Expand Up @@ -120,10 +121,16 @@ export function resolveModelRuntime(
const parallelToolCalls = resolveParallelToolCalls(connection, modelId, adapter);
return {
adapter,
// A Responses relay resolves its send and probe addresses from the same
// API base that model discovery uses in `fetchProviderModelsStrict`, so a
// configured host root cannot answer `/responses` while `/models` 404s
// (#3320).
baseUrl:
connection.providerType === 'kimi-coding-plan' && apiProtocol === 'openai-chat'
? kimiOpenAiBaseUrl(resolvedBaseUrl)
: resolvedBaseUrl,
: connection.providerType === 'openai-responses-compatible'
? responsesRelayApiBaseUrl(resolvedBaseUrl)
: resolvedBaseUrl,
...(apiProtocol ? { apiProtocol } : {}),
wire,
reasoningReplay: reasoningReplayContract(adapter, wire),
Expand Down
29 changes: 29 additions & 0 deletions packages/runtime/src/provider-urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,35 @@ export function openAiResponsesBaseUrl(baseUrl: string): string {
return url.toString();
}

/**
* A custom OpenAI Responses relay is configured with a single URL that has to
* serve both `…/responses` (send and probe) and `…/models` (discovery). Users
* paste the host root — `http://relay.example:3000` — which mainstream relays
* mount under `/v1`, so `/responses` answers at the root while `/models` 404s
* and the model catalog stops refreshing behind a passing probe (#3320).
*
* Normalize the configured value into the one API base every relay call site
* derives from: a path-less root self-heals to `/v1`, as `anthropicV1BaseUrl`
* and `googleV1BetaBaseUrl` already do for their versions; an authored path
* (`/v1`, `/relay/v1`) is preserved verbatim; the endpoint form reduces to its
* base. Callers scope this to the relay provider, because built-ins such as
* DeepSeek legitimately serve `/responses` and `/models` at their own root.
*
* A value that is not a parsable absolute URL is returned untouched: there is
* no base to normalize, and the request it feeds fails where it fails today.
*/
export function responsesRelayApiBaseUrl(baseUrl: string): string {
let url: URL;
try {
url = new URL(baseUrl);
} catch {
return baseUrl;
}
const basePath = stripTrailing(url.pathname).replace(/\/responses$/i, '');
url.pathname = basePath === '' ? '/v1' : basePath;
return url.toString();
}

function stripTrailing(u: string): string {
return u.replace(/\/+$/, '');
}