From cc841797aff837cdda8dabcd5cd7c29bd7ef3426 Mon Sep 17 00:00:00 2001 From: Agent <_@pepijn.ai> Date: Sun, 30 Aug 2026 22:00:58 +0000 Subject: [PATCH] Price sessions that use a custom Responses endpoint A custom Responses endpoint (Azure AI Foundry, or OpenAI direct) built its model with the provider name `custom-openai-responses`. The pricing catalog has no such provider, so `getModelInfo` returned early and every lookup missed. A pricing miss is reported as `undefined`, not an error. CodeLayer hosts drop the cost when it is undefined, so these sessions recorded token counts and no dollars. The failure was silent, and it flattered the numbers: the work still counted in cost-per-unit denominators while adding nothing to the spend. The models behind the endpoint are OpenAI catalog models. `CODELAYER_CODEX_MODEL` renames the model only on the wire, so the model id is still the selected catalog id. Map the provider to `openai`, the same way Codex providers already map. The endpoint serves the public Responses API, not the private Codex one, so it is deliberately left out of the Codex context-window override and keeps the public window. This also gives these sessions a context limit for the first time, so auto-compaction now applies to them. Export the provider name and use it in CodeLayer. A rename on either side used to break pricing with no signal. Co-Authored-By: Claude Opus 5 --- agents/codelayer/src/agent.ts | 3 ++- agents/codelayer/src/providers.ts | 7 ++++--- packages/agentlayer-core/src/index.ts | 1 + packages/agentlayer-core/src/models.ts | 16 ++++++++++++++-- packages/agentlayer-core/test/models.test.ts | 15 ++++++++++++++- 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/agents/codelayer/src/agent.ts b/agents/codelayer/src/agent.ts index 91cd2c8..3f4decb 100644 --- a/agents/codelayer/src/agent.ts +++ b/agents/codelayer/src/agent.ts @@ -2,6 +2,7 @@ import type { LanguageModel, JSONValue } from 'ai' import { createHash } from 'node:crypto' import { Agent, + CUSTOM_RESPONSES_PROVIDER, doomLoop, tarsPersona, type AgentConfig, @@ -218,7 +219,7 @@ export function buildProviderOptions( fastMode: overrides.codex?.fastMode ?? false, ...overrides.codex, } - const isCustomResponses = (model as { provider?: string }).provider === 'custom-openai-responses' + const isCustomResponses = (model as { provider?: string }).provider === CUSTOM_RESPONSES_PROVIDER const openaiOptions = isCustomResponses ? (({ fastMode: _fastMode, serviceTier: _serviceTier, ...options }) => ({ ...options, forceReasoning: true }))(codexOptions) : codexOptions diff --git a/agents/codelayer/src/providers.ts b/agents/codelayer/src/providers.ts index 04f1a9f..bc64d8c 100644 --- a/agents/codelayer/src/providers.ts +++ b/agents/codelayer/src/providers.ts @@ -1,6 +1,7 @@ import { createAnthropic } from '@ai-sdk/anthropic' import { createOpenAI } from '@ai-sdk/openai' import type { LanguageModel } from 'ai' +import { CUSTOM_RESPONSES_PROVIDER } from '@humanlayer/agentlayer-core' import { ensureFileAuthStore, type AuthInfo } from '@humanlayer/agentlayer-provider-auth' import { createCopilotProvider } from '@humanlayer/agentlayer-provider-github-copilot' import { @@ -275,7 +276,7 @@ function reportCustomResponsesError(options: { error: safeMessage, errorName: error.name, operation: options.operation, - provider: 'custom-openai-responses', + provider: CUSTOM_RESPONSES_PROVIDER, statusCode, }, }) @@ -302,7 +303,7 @@ export function createCustomCodexResponsesModel(options: { return await captureResponseUsage(response, rawUsage) } return createOpenAI({ - name: 'custom-openai-responses', + name: CUSTOM_RESPONSES_PROVIDER, baseURL: override.baseURL, apiKey: override.apiKey, fetch: requestFetch as typeof globalThis.fetch, @@ -312,7 +313,7 @@ export function createCustomCodexResponsesModel(options: { return { specificationVersion: modelMetadata.specificationVersion, - provider: 'custom-openai-responses', + provider: CUSTOM_RESPONSES_PROVIDER, modelId: selectedModelId, supportedUrls: modelMetadata.supportedUrls, doGenerate: async (request) => { diff --git a/packages/agentlayer-core/src/index.ts b/packages/agentlayer-core/src/index.ts index 4dedc4b..e92ba76 100644 --- a/packages/agentlayer-core/src/index.ts +++ b/packages/agentlayer-core/src/index.ts @@ -92,6 +92,7 @@ export { toolResultMessage, userMessage, } from './messages' +export { CUSTOM_RESPONSES_PROVIDER } from './models' export { createOutputRenderer, type OutputRenderer, type OutputRendererOptions } from './output-renderer' export { getPendingToolCalls } from './pending' export * from './prompts' diff --git a/packages/agentlayer-core/src/models.ts b/packages/agentlayer-core/src/models.ts index edda5b4..fa205ce 100644 --- a/packages/agentlayer-core/src/models.ts +++ b/packages/agentlayer-core/src/models.ts @@ -55,6 +55,15 @@ export function getCodexContextWindow(modelId: string): number { return CODEX_CONTEXT_WINDOWS[modelId as CodexModel] ?? CODEX_CONTEXT_WINDOWS['gpt-5.5'] } +/** + * Provider name for a user-supplied OpenAI-compatible Responses endpoint (Azure AI + * Foundry, or OpenAI direct). The models behind it are OpenAI catalog models, so the + * key must resolve to `openai` here — anything else silently costs nothing, because a + * pricing miss is reported as `undefined` rather than an error. CodeLayer builds the + * model with this exact name; import it from there rather than repeating the literal. + */ +export const CUSTOM_RESPONSES_PROVIDER = 'custom-openai-responses' + const PROVIDER_LIMIT_OVERRIDES: Record> = {} function getProviderLimitOverride(modelKey: ModelKey): Partial | undefined { @@ -108,8 +117,11 @@ export class ModelProvider { // AI SDK provider keys include a suffix (e.g. "anthropic.messages", "openai.chat") // but models.dev uses the base provider name (e.g. "anthropic", "openai") const baseKey = rawProviderKey.split('.')[0]! - // Codex providers use custom names but their models are OpenAI models - const providerKey = baseKey.startsWith('codex') ? 'openai' : baseKey + // Codex providers use custom names but their models are OpenAI models. So is the model + // behind a custom Responses endpoint: CODELAYER_CODEX_MODEL renames it only on the wire, + // so modelId here is still the selected OpenAI catalog id. + const providerKey = + baseKey.startsWith('codex') || baseKey === CUSTOM_RESPONSES_PROVIDER ? 'openai' : baseKey const provider = this.modelsData[providerKey] if (!provider?.models) return undefined diff --git a/packages/agentlayer-core/test/models.test.ts b/packages/agentlayer-core/test/models.test.ts index 5378610..f9fad97 100644 --- a/packages/agentlayer-core/test/models.test.ts +++ b/packages/agentlayer-core/test/models.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'bun:test' -import { CODEX_CONTEXT_WINDOWS, type ModelKey, ModelProvider } from '../src/models' +import { CODEX_CONTEXT_WINDOWS, CUSTOM_RESPONSES_PROVIDER, type ModelKey, ModelProvider } from '../src/models' describe('ModelProvider.getModelLimits', () => { const provider = new ModelProvider() @@ -35,6 +35,19 @@ describe('ModelProvider.getModelLimits', () => { expect(limits?.output).toBe(128_000) }) + test('a custom Responses endpoint is priced as the OpenAI model it serves', () => { + // Regression: this key used to miss the catalog entirely, and a pricing miss is + // reported as `undefined`, so Azure AI Foundry sessions recorded tokens and no + // dollars at all — silently, and unrecoverably, since cost is frozen at ingest. + expect(provider.getModelPricing(`${CUSTOM_RESPONSES_PROVIDER}/gpt-5.6-sol`)).toMatchObject({ + input: 5, + output: 30, + }) + + // The public Responses API, not the private Codex one, so it keeps the public window. + expect(provider.getModelLimits(`${CUSTOM_RESPONSES_PROVIDER}/gpt-5.6-sol`)?.context).toBe(1_050_000) + }) + test('openai/gpt-5.6 keeps the public OpenAI API context window and pricing', () => { const limits = provider.getModelLimits('openai/gpt-5.6-sol')