diff --git a/apps/server/package.json b/apps/server/package.json index c19bacb4..5a7bce5a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,8 +24,8 @@ "dependencies": { "@effect/platform-node": "catalog:", "@effect/sql-sqlite-bun": "catalog:", - "@github/copilot": "1.0.2", - "@github/copilot-sdk": "0.1.31-unstable.0", + "@github/copilot": "1.0.7", + "@github/copilot-sdk": "0.1.32", "@pierre/diffs": "^1.1.0-beta.16", "effect": "catalog:", "node-pty": "^1.1.0", diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 128b174c..a124175c 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -2,6 +2,7 @@ import { EventId, TurnId, type OrchestrationThreadActivity } from "@t3tools/cont import { describe, expect, it } from "vitest"; import { deriveVisibleThreadWorkLogEntries, + orderCopilotBuiltInModelOptions, resolveProviderHealthBannerProvider, } from "./ChatView.logic"; @@ -47,6 +48,29 @@ describe("resolveProviderHealthBannerProvider", () => { }); }); +describe("orderCopilotBuiltInModelOptions", () => { + it("reorders runtime copilot models to match the preferred built-in picker order", () => { + expect( + orderCopilotBuiltInModelOptions([ + { slug: "gpt-5.4", name: "GPT-5.4" }, + { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, + { slug: "gpt-5.4-mini", name: "GPT-5.4 mini" }, + { slug: "gpt-5.2", name: "GPT-5.2" }, + ]).map((option) => option.slug), + ).toEqual(["gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.2"]); + }); + + it("keeps unknown runtime-only models after the preferred built-in models", () => { + expect( + orderCopilotBuiltInModelOptions([ + { slug: "gpt-5.4", name: "GPT-5.4" }, + { slug: "future-runtime-model", name: "Future Runtime Model" }, + { slug: "gpt-5.4-mini", name: "GPT-5.4 mini" }, + ]).map((option) => option.slug), + ).toEqual(["gpt-5.4", "gpt-5.4-mini", "future-runtime-model"]); + }); +}); + describe("deriveVisibleThreadWorkLogEntries", () => { it("keeps completed tool calls from previous turns visible in the thread timeline", () => { const activities: OrchestrationThreadActivity[] = [ diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 7a1c60a3..15eaadcd 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -4,9 +4,10 @@ import { type ProviderKind, type ThreadId, } from "@t3tools/contracts"; +import { getModelOptions } from "@t3tools/shared/model"; import { type ChatMessage, type Thread } from "../types"; import { randomUUID } from "~/lib/utils"; -import { getAppModelOptions } from "../appSettings"; +import { getAppModelOptions, type BuiltInAppModelOption } from "../appSettings"; import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore"; import { Schema } from "effect"; import { deriveWorkLogEntries, type WorkLogEntry } from "../session-logic"; @@ -132,6 +133,34 @@ export function getCustomModelOptionsByProvider(settings: { }; } +export function orderCopilotBuiltInModelOptions( + runtimeOptions: ReadonlyArray, + preferredOptions: ReadonlyArray = getModelOptions("copilot"), +): ReadonlyArray { + const preferredIndexBySlug = new Map( + preferredOptions.map((option, index) => [option.slug, index] as const), + ); + + return runtimeOptions + .map((option, runtimeIndex) => ({ option, runtimeIndex })) + .toSorted((left, right) => { + const leftPreferredIndex = preferredIndexBySlug.get(left.option.slug); + const rightPreferredIndex = preferredIndexBySlug.get(right.option.slug); + + if (leftPreferredIndex !== undefined && rightPreferredIndex !== undefined) { + return leftPreferredIndex - rightPreferredIndex; + } + if (leftPreferredIndex !== undefined) { + return -1; + } + if (rightPreferredIndex !== undefined) { + return 1; + } + return left.runtimeIndex - right.runtimeIndex; + }) + .map(({ option }) => option); +} + export function resolveProviderHealthBannerProvider(input: { sessionProvider: ProviderKind | null; selectedProvider: ProviderKind; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 2a9a2760..7717d580 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -250,6 +250,7 @@ import { } from "./chat/MessagesTimeline.logic"; import { deriveVisibleThreadWorkLogEntries, + orderCopilotBuiltInModelOptions, resolveProviderHealthBannerProvider, } from "./ChatView.logic"; @@ -1401,7 +1402,9 @@ export default function ChatView({ threadId }: ChatViewProps) { codex: getModelOptions("codex"), copilot: copilotProviderModels.length > 0 - ? copilotProviderModels.map((model) => ({ slug: model.id, name: model.name })) + ? orderCopilotBuiltInModelOptions( + copilotProviderModels.map((model) => ({ slug: model.id, name: model.name })), + ) : getModelOptions("copilot"), }), [copilotProviderModels], diff --git a/bun.lock b/bun.lock index a2f0aa47..646619b7 100644 --- a/bun.lock +++ b/bun.lock @@ -51,8 +51,8 @@ "dependencies": { "@effect/platform-node": "catalog:", "@effect/sql-sqlite-bun": "catalog:", - "@github/copilot": "1.0.2", - "@github/copilot-sdk": "0.1.31-unstable.0", + "@github/copilot": "1.0.7", + "@github/copilot-sdk": "0.1.32", "@pierre/diffs": "^1.1.0-beta.16", "effect": "catalog:", "node-pty": "^1.1.0", @@ -358,21 +358,21 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], - "@github/copilot": ["@github/copilot@1.0.2", "", { "optionalDependencies": { "@github/copilot-darwin-arm64": "1.0.2", "@github/copilot-darwin-x64": "1.0.2", "@github/copilot-linux-arm64": "1.0.2", "@github/copilot-linux-x64": "1.0.2", "@github/copilot-win32-arm64": "1.0.2", "@github/copilot-win32-x64": "1.0.2" }, "bin": { "copilot": "npm-loader.js" } }, "sha512-716SIZMYftldVcJay2uZOzsa9ROGGb2Mh2HnxbDxoisFsWNNgZlQXlV7A+PYoGsnAo2Zk/8e1i5SPTscGf2oww=="], + "@github/copilot": ["@github/copilot@1.0.7", "", { "optionalDependencies": { "@github/copilot-darwin-arm64": "1.0.7", "@github/copilot-darwin-x64": "1.0.7", "@github/copilot-linux-arm64": "1.0.7", "@github/copilot-linux-x64": "1.0.7", "@github/copilot-win32-arm64": "1.0.7", "@github/copilot-win32-x64": "1.0.7" }, "bin": { "copilot": "npm-loader.js" } }, "sha512-KHBaJ1kbc19pqUMnB9LubPtwWVOaDCzWbzwsJss+DvHyCpr8wP8jR3GEZUnhq3rsuXI96ZKEeEozXM0NqxCAiw=="], - "@github/copilot-darwin-arm64": ["@github/copilot-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64", "bin": { "copilot-darwin-arm64": "copilot" } }, "sha512-dYoeaTidsphRXyMjvAgpjEbBV41ipICnXURrLFEiATcjC4IY6x2BqPOocrExBYW/Tz2VZvDw51iIZaf6GXrTmw=="], + "@github/copilot-darwin-arm64": ["@github/copilot-darwin-arm64@1.0.7", "", { "os": "darwin", "cpu": "arm64", "bin": { "copilot-darwin-arm64": "copilot" } }, "sha512-yQITowpkQYamww59CwcG5JTWV9ahj7nMH6oqObMJaeqXnG7j7dqE/YhLkujQZ3XR8VXAoIa1rZ3TahdMu94gOA=="], - "@github/copilot-darwin-x64": ["@github/copilot-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64", "bin": { "copilot-darwin-x64": "copilot" } }, "sha512-8+Z9dYigEfXf0wHl9c2tgFn8Cr6v4RAY8xTgHMI9mZInjQyxVeBXCxbE2VgzUtDUD3a705Ka2d8ZOz05aYtGsg=="], + "@github/copilot-darwin-x64": ["@github/copilot-darwin-x64@1.0.7", "", { "os": "darwin", "cpu": "x64", "bin": { "copilot-darwin-x64": "copilot" } }, "sha512-23vP5bHaFA030nB3tr+dUUdRm2SqmQbs2fZUQ4F7JeYy59jp9hi8lBdaZp/TeQnjEirAUU9H2HZxsGRIIUWp7g=="], - "@github/copilot-linux-arm64": ["@github/copilot-linux-arm64@1.0.2", "", { "os": "linux", "cpu": "arm64", "bin": { "copilot-linux-arm64": "copilot" } }, "sha512-ik0Y5aTXOFRPLFrNjZJdtfzkozYqYeJjVXGBAH3Pp1nFZRu/pxJnrnQ1HrqO/LEgQVbJzAjQmWEfMbXdQIxE4Q=="], + "@github/copilot-linux-arm64": ["@github/copilot-linux-arm64@1.0.7", "", { "os": "linux", "cpu": "arm64", "bin": { "copilot-linux-arm64": "copilot" } }, "sha512-g0mB98oyXKcpd4sMNBc5n1h3UhLy9AGRlT//VL8BXPSzvlTH/dJP3fdx74pbLSgvz105to/YUMmEAFfv25VNaw=="], - "@github/copilot-linux-x64": ["@github/copilot-linux-x64@1.0.2", "", { "os": "linux", "cpu": "x64", "bin": { "copilot-linux-x64": "copilot" } }, "sha512-mHSPZjH4nU9rwbfwLxYJ7CQ90jK/Qu1v2CmvBCUPfmuGdVwrpGPHB5FrB+f+b0NEXjmemDWstk2zG53F7ppHfw=="], + "@github/copilot-linux-x64": ["@github/copilot-linux-x64@1.0.7", "", { "os": "linux", "cpu": "x64", "bin": { "copilot-linux-x64": "copilot" } }, "sha512-TRxzvTo9I4ehYJLFHTCJSJYQ4QnO/V9zebqwszxHpJRxuBd7FV4cxLmfOBqZcUpEpZgBH+VJ4OG98BPW7YEtJQ=="], - "@github/copilot-sdk": ["@github/copilot-sdk@0.1.31-unstable.0", "", { "dependencies": { "@github/copilot": "^0.0.421", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" } }, "sha512-vQUH+dfY1n9rkmpkueKU6jnW1SgQWw8hENwPTfQLkAThWaYwjerVLq3ArW+Lhq5gAddzWny4bOSQ7tBI8KF8OA=="], + "@github/copilot-sdk": ["@github/copilot-sdk@0.1.32", "", { "dependencies": { "@github/copilot": "^1.0.2", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" } }, "sha512-mPWM0fw1Gqc/SW8nl45K8abrFH+92fO7y6tRtRl5imjS5hGapLf/dkX5WDrgPtlsflD0c41lFXVUri5NVJwtoA=="], - "@github/copilot-win32-arm64": ["@github/copilot-win32-arm64@1.0.2", "", { "os": "win32", "cpu": "arm64", "bin": { "copilot-win32-arm64": "copilot.exe" } }, "sha512-tLW2CY/vg0fYLp8EuiFhWIHBVzbFCDDpohxT/F/XyMAdTVSZLnopCcxQHv2BOu0CVGrYjlf7YOIwPfAKYml1FA=="], + "@github/copilot-win32-arm64": ["@github/copilot-win32-arm64@1.0.7", "", { "os": "win32", "cpu": "arm64", "bin": { "copilot-win32-arm64": "copilot.exe" } }, "sha512-4yFgW1K0MlKBrK5BwMIj4nMu5KSFfytNXrs8iOpVgp7erEvKVyN7VXb6SWkoU3M9TfeNlqP6Uje2rxDvgR1u5w=="], - "@github/copilot-win32-x64": ["@github/copilot-win32-x64@1.0.2", "", { "os": "win32", "cpu": "x64", "bin": { "copilot-win32-x64": "copilot.exe" } }, "sha512-cFlc3xMkKKFRIYR00EEJ2XlYAemeh5EZHsGA8Ir2G0AH+DOevJbomdP1yyCC5gaK/7IyPkHX3sGie5sER2yPvQ=="], + "@github/copilot-win32-x64": ["@github/copilot-win32-x64@1.0.7", "", { "os": "win32", "cpu": "x64", "bin": { "copilot-win32-x64": "copilot.exe" } }, "sha512-RDZlvPf/q6B54wLXJRmI39fc9+pwfcAjSwUqw0FeQruCTQgoUl8eo9NqeVWDFlr3RdzgVSMUiJHc3aiifVG6lA=="], "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="], @@ -1930,8 +1930,6 @@ "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@github/copilot-sdk/@github/copilot": ["@github/copilot@0.0.421", "", { "optionalDependencies": { "@github/copilot-darwin-arm64": "0.0.421", "@github/copilot-darwin-x64": "0.0.421", "@github/copilot-linux-arm64": "0.0.421", "@github/copilot-linux-x64": "0.0.421", "@github/copilot-win32-arm64": "0.0.421", "@github/copilot-win32-x64": "0.0.421" }, "bin": { "copilot": "npm-loader.js" } }, "sha512-nDUt9f5al7IgBOTc7AwLpqvaX61VsRDYDQ9D5iR0QQzHo4pgDcyOXIjXUQUKsJwObXHfh6qR+Jm1vnlbw5cacg=="], - "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], @@ -2026,18 +2024,6 @@ "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - "@github/copilot-sdk/@github/copilot/@github/copilot-darwin-arm64": ["@github/copilot-darwin-arm64@0.0.421", "", { "os": "darwin", "cpu": "arm64", "bin": { "copilot-darwin-arm64": "copilot" } }, "sha512-S4plFsxH7W8X1gEkGNcfyKykIji4mNv8BP/GpPs2Ad84qWoJpZzfZsjrjF0BQ8mvFObWp6Ft2SZOnJzFZW1Ftw=="], - - "@github/copilot-sdk/@github/copilot/@github/copilot-darwin-x64": ["@github/copilot-darwin-x64@0.0.421", "", { "os": "darwin", "cpu": "x64", "bin": { "copilot-darwin-x64": "copilot" } }, "sha512-h+Dbfq8ByAielLYIeJbjkN/9Abs6AKHFi+XuuzEy4YA9jOA42uKMFsWYwaoYH8ZLK9Y+4wagYI9UewVPnyIWPA=="], - - "@github/copilot-sdk/@github/copilot/@github/copilot-linux-arm64": ["@github/copilot-linux-arm64@0.0.421", "", { "os": "linux", "cpu": "arm64", "bin": { "copilot-linux-arm64": "copilot" } }, "sha512-cxlqDRR/wKfbdzd456N2h7sZOZY069wU2ycSYSmo7cC75U5DyhMGYAZwyAhvQ7UKmS5gJC/wgSgye0njuK22Xg=="], - - "@github/copilot-sdk/@github/copilot/@github/copilot-linux-x64": ["@github/copilot-linux-x64@0.0.421", "", { "os": "linux", "cpu": "x64", "bin": { "copilot-linux-x64": "copilot" } }, "sha512-7np5b6EEemJ3U3jnl92buJ88nlpqOAIrLaJxx3pJGrP9SVFMBD/6EAlfIQ5m5QTfs+/vIuTKWBrq1wpFVZZUcQ=="], - - "@github/copilot-sdk/@github/copilot/@github/copilot-win32-arm64": ["@github/copilot-win32-arm64@0.0.421", "", { "os": "win32", "cpu": "arm64", "bin": { "copilot-win32-arm64": "copilot.exe" } }, "sha512-T6qCqOnijD5pmC0ytVsahX3bpDnXtLTgo9xFGo/BGaPEvX02ePkzcRZkfkOclkzc8QlkVji6KqZYB+qMZTliwg=="], - - "@github/copilot-sdk/@github/copilot/@github/copilot-win32-x64": ["@github/copilot-win32-x64@0.0.421", "", { "os": "win32", "cpu": "x64", "bin": { "copilot-win32-x64": "copilot.exe" } }, "sha512-KDfy3wsRQFIcOQDdd5Mblvh+DWRq+UGbTQ34wyW36ws1BsdWkV++gk9bTkeJRsPbQ51wsJ0V/jRKEZv4uK5dTA=="], - "@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], "@pierre/diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index af7fb956..6b01a325 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -35,6 +35,7 @@ export const MODEL_OPTIONS_BY_PROVIDER = { ], copilot: [ { slug: "gpt-5.4", name: "GPT-5.4" }, + { slug: "gpt-5.4-mini", name: "GPT-5.4 mini" }, { slug: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" }, { slug: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" }, { slug: "claude-haiku-4.5", name: "Claude Haiku 4.5" }, @@ -43,6 +44,7 @@ export const MODEL_OPTIONS_BY_PROVIDER = { { slug: "claude-opus-4.5", name: "Claude Opus 4.5" }, { slug: "claude-sonnet-4", name: "Claude Sonnet 4" }, { slug: "gemini-3-pro-preview", name: "Gemini 3 Pro (Preview)" }, + { slug: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, { slug: "gpt-5.2", name: "GPT-5.2" }, @@ -52,6 +54,7 @@ export const MODEL_OPTIONS_BY_PROVIDER = { { slug: "gpt-5.1", name: "GPT-5.1" }, { slug: "gpt-5-mini", name: "GPT-5 mini" }, { slug: "gpt-4.1", name: "GPT-4.1" }, + { slug: "raptor-mini", name: "Raptor mini" }, ], } as const satisfies Record; export type ModelOptionsByProvider = typeof MODEL_OPTIONS_BY_PROVIDER; @@ -75,6 +78,7 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER = { copilot: { "4.1": "gpt-4.1", "5.4": "gpt-5.4", + "5.4-mini": "gpt-5.4-mini", "5-mini": "gpt-5-mini", "5.1": "gpt-5.1", "5.1-codex": "gpt-5.1-codex", @@ -87,6 +91,8 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER = { sonnet: "claude-sonnet-4.6", opus: "claude-opus-4.6", gemini: "gemini-3-pro-preview", + "gemini-3.1": "gemini-3.1-pro", + raptor: "raptor-mini", }, } as const satisfies Record>; diff --git a/packages/shared/src/model.test.ts b/packages/shared/src/model.test.ts index 8771a24c..18334f2c 100644 --- a/packages/shared/src/model.test.ts +++ b/packages/shared/src/model.test.ts @@ -16,6 +16,12 @@ describe("normalizeModelSlug", () => { expect(normalizeModelSlug("gpt-5.3")).toBe("gpt-5.3-codex"); }); + it("maps copilot aliases to canonical slugs", () => { + expect(normalizeModelSlug("5.4-mini", "copilot")).toBe("gpt-5.4-mini"); + expect(normalizeModelSlug("gemini-3.1", "copilot")).toBe("gemini-3.1-pro"); + expect(normalizeModelSlug("raptor", "copilot")).toBe("raptor-mini"); + }); + it("returns null for empty or missing values", () => { expect(normalizeModelSlug("")).toBeNull(); expect(normalizeModelSlug(" ")).toBeNull(); @@ -50,6 +56,13 @@ describe("resolveModelSlug", () => { expect(resolveModelSlug(model.slug)).toBe(model.slug); } }); + + it("resolves supported copilot model options", () => { + for (const model of MODEL_OPTIONS_BY_PROVIDER.copilot) { + expect(resolveModelSlug(model.slug, "copilot")).toBe(model.slug); + } + }); + it("keeps codex defaults for backward compatibility", () => { expect(getDefaultModel()).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); expect(getModelOptions()).toEqual(MODEL_OPTIONS_BY_PROVIDER.codex);