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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/cli/ai/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ const AI_PROVIDER_DEFINITIONS: Record< AiProviderId, AiProviderDefinition > = {
}
env.ANTHROPIC_CUSTOM_HEADERS = buildAnthropicCustomHeaders( anthropicHeaders );

// OpenAI completions path. The wpcom proxy accepts the same bearer token and
// OpenAI Responses path. The wpcom proxy accepts the same bearer token and
// dispatches to the right upstream based on the request path.
// @ai-sdk/openai expects baseURL to include /v1 (like the real
// OpenAI API), so the request path becomes /v1/chat/completions
// The OpenAI SDK expects baseURL to include /v1 (like the real
// OpenAI API), so the request path becomes /v1/responses
// mirroring the Anthropic path's /v1/messages.
env.OPENAI_BASE_URL = `${ gatewayBaseUrl.replace( /\/+$/, '' ) }/v1`;
env.OPENAI_API_KEY = accessToken;
Expand Down
19 changes: 14 additions & 5 deletions apps/cli/ai/runtimes/pi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import type { AskUserHandler, SiteInfo } from 'cli/ai/types';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AgentToolAny = AgentTool< any >;
type StudioModel = Model< 'openai-completions' > | Model< 'anthropic-messages' >;
type StudioModel = Model< 'openai-responses' > | Model< 'anthropic-messages' >;
type ProviderConfigInput = Parameters< ModelRegistry[ 'registerProvider' ] >[ 1 ];

const STUDIO_WPCOM_ANTHROPIC_PROVIDER = 'studio-wpcom-anthropic';
Expand Down Expand Up @@ -363,13 +363,22 @@ function buildModel(
};

if ( family === 'openai' ) {
// GPT-5.6 models reject function tools on /v1/chat/completions unless
// reasoning is disabled; the Responses API supports tools + reasoning.
// GPT-5.6 Sol's real context window is 1.05M tokens, but we declare
// 272K — the threshold where OpenAI's 2x long-context pricing kicks
// in — so compaction keeps sessions below it. Understating the window
// is also load-bearing for correctness: pi clamps max output tokens to
// the declared window minus its (post-compaction, sometimes stale)
// context estimate, and a too-small window can clamp all the way down
// to 1, which the API rejects with a 400.
return {
...common,
api: 'openai-completions',
api: 'openai-responses',
provider: 'openai',
reasoning: false,
contextWindow: 200_000,
maxTokens: 16_384,
reasoning: true,
contextWindow: 272_000,
maxTokens: 32_000,
};
}
return {
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/ai/slash-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,8 @@ export const AI_CHAT_SLASH_COMMANDS: SlashCommandDef[] = [
// Build options and a reverse lookup at the same time so we never
// have to recover the model id from the label. A startsWith-based
// match is buggy when one model's label is a prefix of another's
// (e.g. "GPT 5.5" prefixes "GPT 5.5 Pro" — picking Pro silently
// returns the non-pro id), so we keep the label → id mapping
// (e.g. "GPT 5.6" prefixes "GPT 5.6 Sol" — picking Sol silently
// returns the other id), so we keep the label → id mapping
// explicit here and look up by exact match below.
const labelToId = new Map< string, AiModelId >();
const modelOptions = availableModels.map( ( id ) => {
Expand Down
30 changes: 15 additions & 15 deletions apps/cli/ai/tests/pi-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ const DEFAULT_MOCK_EVENTS: AgentSessionEvent[] = [
message: {
role: 'assistant',
content: [ { type: 'text', text: 'mocked openai response' } ],
api: 'openai-completions',
api: 'openai-responses',
provider: 'openai',
model: 'gpt-5.5',
model: 'gpt-5.6-sol',
usage: {
input: 0,
output: 0,
Expand All @@ -85,9 +85,9 @@ const DEFAULT_MOCK_EVENTS: AgentSessionEvent[] = [
message: {
role: 'assistant',
content: [ { type: 'text', text: 'mocked openai response' } ],
api: 'openai-completions',
api: 'openai-responses',
provider: 'openai',
model: 'gpt-5.5',
model: 'gpt-5.6-sol',
usage: {
input: 0,
output: 0,
Expand Down Expand Up @@ -115,9 +115,9 @@ const assistantMessage = (
message: {
role: 'assistant',
content,
api: 'openai-completions',
api: 'openai-responses',
provider: 'openai',
model: 'gpt-5.5',
model: 'gpt-5.6-sol',
usage: {
input: 0,
output: 0,
Expand Down Expand Up @@ -224,7 +224,7 @@ describe( 'pi runtime', () => {
const events = await runRuntime( {
prompt: 'hello',
env: {},
model: 'gpt-5.5',
model: 'gpt-5.6-sol',
session: newSession(),
} );

Expand All @@ -249,7 +249,7 @@ describe( 'pi runtime', () => {
OPENAI_API_KEY: 'sk-test',
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
},
model: 'gpt-5.5',
model: 'gpt-5.6-sol',
session: newSession(),
} );

Expand All @@ -265,7 +265,7 @@ describe( 'pi runtime', () => {
OPENAI_API_KEY: 'sk-test',
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
},
model: 'gpt-5.5',
model: 'gpt-5.6-sol',
session: newSession(),
} );

Expand All @@ -279,7 +279,7 @@ describe( 'pi runtime', () => {
OPENAI_API_KEY: 'sk-test',
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
},
model: 'gpt-5.5',
model: 'gpt-5.6-sol',
session: newSession(),
} );

Expand Down Expand Up @@ -336,7 +336,7 @@ describe( 'pi runtime', () => {
OPENAI_API_KEY: 'sk-test',
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
},
model: 'gpt-5.5',
model: 'gpt-5.6-sol',
session: newSession(),
activeSite: {
name: 'Remote',
Expand Down Expand Up @@ -367,7 +367,7 @@ describe( 'pi runtime', () => {
OPENAI_API_KEY: 'sk-test',
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
},
model: 'gpt-5.5',
model: 'gpt-5.6-sol',
session: newSession(),
} );

Expand All @@ -388,7 +388,7 @@ describe( 'pi runtime', () => {
const session = newSession();
const otherOpenAiModel = 'gpt-test-other' as AiModelId;

await runRuntime( { prompt: 'hi', env, model: 'gpt-5.5', session } );
await runRuntime( { prompt: 'hi', env, model: 'gpt-5.6-sol', session } );
await runRuntime( { prompt: 'follow-up', env, model: otherOpenAiModel, session } );
await runRuntime( {
prompt: 'still on the second model',
Expand All @@ -398,7 +398,7 @@ describe( 'pi runtime', () => {
} );

expect( mocks.createdSessions.map( ( s ) => s.state.model.id ) ).toEqual( [
'gpt-5.5',
'gpt-5.6-sol',
otherOpenAiModel,
otherOpenAiModel,
] );
Expand Down Expand Up @@ -469,7 +469,7 @@ describe( 'pi runtime', () => {
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
STUDIO_OPENAI_DEFAULT_HEADERS: '{not json',
},
model: 'gpt-5.5',
model: 'gpt-5.6-sol',
session: newSession(),
} );

Expand Down
14 changes: 7 additions & 7 deletions apps/cli/ai/tests/slash-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,12 +351,12 @@ function buildModelCtx(
// `as never` keeps this test framework-agnostic — we don't need a
// real AiChatUI, just the methods the /model handler touches.
ui: {
currentModel: overrides.currentModel ?? 'gpt-5.5',
currentModel: overrides.currentModel ?? 'gpt-5.6-sol',
askUser: vi.fn().mockResolvedValue( { 0: overrides.askUserResponse } ),
showInfo: vi.fn(),
showError: vi.fn(),
} as never,
currentModel: overrides.currentModel ?? 'gpt-5.5',
currentModel: overrides.currentModel ?? 'gpt-5.6-sol',
currentProvider: 'wpcom',
showCapabilitiesOnConnect: false,
switchProvider: vi.fn().mockResolvedValue( undefined ),
Expand All @@ -372,13 +372,13 @@ describe( '/model slash command', () => {
// Locks in the labelToId-map matcher introduced in slash-commands.ts.
// The previous implementation used `selectedLabel.startsWith( label )`,
// which silently picked the wrong id whenever one model's label was a
// prefix of another's (e.g. "GPT 5.5" prefix of "GPT 5.5 Pro"). We don't
// prefix of another's (e.g. "GPT 5.6" prefix of "GPT 5.6 Sol"). We don't
// currently expose any prefix-colliding labels, but the fix is general
// and we want the regression coverage to survive future additions.
it( 'resolves the picked model exactly by label, not by prefix', async () => {
expect( modelHandler ).toBeDefined();
const { ctx, persistMock } = buildModelCtx( {
currentModel: 'gpt-5.5',
currentModel: 'gpt-5.6-sol',
askUserResponse: 'Sonnet 5',
} );

Expand All @@ -390,14 +390,14 @@ describe( '/model slash command', () => {

it( 'still resolves the picked model when its label carries the "(current)" suffix', async () => {
const { ctx, persistMock } = buildModelCtx( {
currentModel: 'gpt-5.5',
askUserResponse: 'GPT 5.5 (current)',
currentModel: 'gpt-5.6-sol',
askUserResponse: 'GPT 5.6 Sol (current)',
} );

await modelHandler!( '/model', ctx );

// Same model picked → no swap, no persist.
expect( ctx.currentModel ).toBe( 'gpt-5.5' );
expect( ctx.currentModel ).toBe( 'gpt-5.6-sol' );
expect( persistMock ).not.toHaveBeenCalled();
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -283,13 +283,13 @@ describe( 'Composer menu', () => {
);

fireEvent.click( screen.getByRole( 'button', { name: 'Select model' } ) );
fireEvent.click( await screen.findByText( 'GPT 5.5' ) );
fireEvent.click( await screen.findByText( 'GPT 5.6 Sol' ) );
fireEvent.click( await screen.findByRole( 'button', { name: 'Start new conversation' } ) );

await waitFor( () => {
expect( onSwitchSession ).toHaveBeenCalledWith( 'fresh-session' );
} );
expect( connectorMocks.setSessionModel ).toHaveBeenCalledWith( 'fresh-session', 'gpt-5.5' );
expect( connectorMocks.setSessionModel ).toHaveBeenCalledWith( 'fresh-session', 'gpt-5.6-sol' );

const loadedSession = queryClient.getQueryData< LoadedAiSession >( [
...SESSIONS_QUERY_KEY,
Expand All @@ -299,7 +299,7 @@ describe( 'Composer menu', () => {
expect( loadedSession?.entries ).toEqual( [
expect.objectContaining( {
type: 'model_change',
modelId: 'gpt-5.5',
modelId: 'gpt-5.6-sol',
} ),
] );
} );
Expand Down
11 changes: 5 additions & 6 deletions packages/common/ai/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ export interface AiModel {
}

// Pro / o-series OpenAI variants (`gpt-*-pro`, `o[1-9]*`) are intentionally
// excluded: they're reasoning-only models that, with our current Chat
// Completions path through the wpcom AI proxy, return HTTP 400 ("only
// supported in v1/responses"). Re-enable once we either route them through
// `/v1/responses` server-side or extend the proxy/SDK timeout window for
// reasoning turns.
// excluded: their long reasoning turns can exceed the proxy/SDK timeout
// window. (Routing is no longer a blocker — the OpenAI family now goes
// through the proxy's `/v1/responses` path, which supports reasoning models
// and function tools.)
export const AI_MODELS = [
{ id: 'claude-sonnet-5', label: 'Sonnet 5', family: 'anthropic' },
{ id: 'claude-opus-4-8', label: 'Opus 4.8', family: 'anthropic' },
{ id: 'gpt-5.5', label: 'GPT 5.5', family: 'openai' },
{ id: 'gpt-5.6-sol', label: 'GPT 5.6 Sol', family: 'openai' },
] as const satisfies readonly AiModel[];

export type AiModelId = ( typeof AI_MODELS )[ number ][ 'id' ];
Expand Down
Loading