Skip to content

Commit b09932d

Browse files
committed
Fix shared model provider inference for first turns
1 parent d1669b9 commit b09932d

4 files changed

Lines changed: 62 additions & 3 deletions

File tree

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ describe("ProviderCommandReactor", () => {
110110
typeof input === "object" &&
111111
input !== null &&
112112
"provider" in input &&
113-
(input.provider === "codex" || input.provider === "claudeAgent")
113+
(input.provider === "codex" ||
114+
input.provider === "copilot" ||
115+
input.provider === "claudeAgent")
114116
? input.provider
115117
: "codex";
116118
const resumeCursor =
@@ -552,6 +554,46 @@ describe("ProviderCommandReactor", () => {
552554
});
553555
});
554556

557+
it("binds a first turn to the requested provider when the model slug is shared", async () => {
558+
const harness = await createHarness({ threadModel: "gpt-5.4" });
559+
const now = new Date().toISOString();
560+
561+
await Effect.runPromise(
562+
harness.engine.dispatch({
563+
type: "thread.turn.start",
564+
commandId: CommandId.makeUnsafe("cmd-turn-start-shared-model-provider"),
565+
threadId: ThreadId.makeUnsafe("thread-1"),
566+
message: {
567+
messageId: asMessageId("user-message-shared-model-provider"),
568+
role: "user",
569+
text: "hello copilot",
570+
attachments: [],
571+
},
572+
provider: "copilot",
573+
model: "gpt-5.4",
574+
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
575+
runtimeMode: "approval-required",
576+
createdAt: now,
577+
}),
578+
);
579+
580+
await waitFor(() => harness.startSession.mock.calls.length === 1);
581+
await waitFor(() => harness.sendTurn.mock.calls.length === 1);
582+
583+
expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
584+
provider: "copilot",
585+
model: "gpt-5.4",
586+
});
587+
expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
588+
threadId: ThreadId.makeUnsafe("thread-1"),
589+
model: "gpt-5.4",
590+
});
591+
592+
const readModel = await Effect.runPromise(harness.engine.getReadModel());
593+
const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));
594+
expect(thread?.session?.providerName).toBe("copilot");
595+
});
596+
555597
it("rejects a turn when the requested model belongs to a different provider", async () => {
556598
const harness = await createHarness();
557599
const now = new Date().toISOString();

apps/server/src/orchestration/Layers/ProviderCommandReactor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ const make = Effect.gen(function* () {
233233
)
234234
? thread.session.providerName
235235
: undefined;
236-
const threadProvider: ProviderKind = currentProvider ?? inferProviderForModel(thread.model);
236+
const threadProvider: ProviderKind =
237+
currentProvider ?? inferProviderForModel(thread.model, options?.provider ?? "codex");
237238
if (options?.provider !== undefined && options.provider !== threadProvider) {
238239
return yield* new ProviderAdapterRequestError({
239240
provider: threadProvider,

packages/shared/src/model.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ describe("inferProviderForModel", () => {
207207
expect(inferProviderForModel("sonnet")).toBe("claudeAgent");
208208
});
209209

210+
it("prefers the fallback provider for shared model slugs", () => {
211+
expect(inferProviderForModel("gpt-5.4", "copilot")).toBe("copilot");
212+
});
213+
210214
it("falls back when the model is unknown", () => {
211215
expect(inferProviderForModel("custom/internal-model")).toBe("codex");
212216
expect(inferProviderForModel("custom/internal-model", "claudeAgent")).toBe("claudeAgent");

packages/shared/src/model.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ export function inferProviderForModel(
141141
model: string | null | undefined,
142142
fallback: ProviderKind = "codex",
143143
): ProviderKind {
144+
const normalizedFallback = normalizeModelSlug(model, fallback);
145+
if (normalizedFallback && MODEL_SLUG_SET_BY_PROVIDER[fallback].has(normalizedFallback)) {
146+
return fallback;
147+
}
148+
144149
const normalizedClaude = normalizeModelSlug(model, "claudeAgent");
145150
if (normalizedClaude && MODEL_SLUG_SET_BY_PROVIDER.claudeAgent.has(normalizedClaude)) {
146151
return "claudeAgent";
@@ -151,7 +156,14 @@ export function inferProviderForModel(
151156
return "codex";
152157
}
153158

154-
return typeof model === "string" && model.trim().startsWith("claude-") ? "claudeAgent" : fallback;
159+
const trimmed = typeof model === "string" ? model.trim() : "";
160+
if (trimmed.startsWith("claude-")) {
161+
return "claudeAgent";
162+
}
163+
if (trimmed.startsWith("gpt-")) {
164+
return "codex";
165+
}
166+
return fallback;
155167
}
156168

157169
export function getReasoningEffortOptions(provider: "codex"): ReadonlyArray<CodexReasoningEffort>;

0 commit comments

Comments
 (0)