-
-
Notifications
You must be signed in to change notification settings - Fork 304
feat: 模型消耗管理与智能匹配 | Model consumption management and intelligent matching #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
2c5676b
d5c538d
2ff9a4b
a4bfefa
f4e1eb8
62a73b7
61326bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| { | ||
| "lockfileVersion": 1, | ||
| "configVersion": 0, | ||
| "workspaces": { | ||
| "": { | ||
| "name": "copilot-api", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| { | ||
| "models": [ | ||
| { | ||
| "name": "GPT-4.1", | ||
| "consumption": "0x" | ||
| }, | ||
| { | ||
| "name": "GPT-4o", | ||
| "consumption": "0x" | ||
| }, | ||
| { | ||
| "name": "GPT-5 mini", | ||
| "consumption": "0x" | ||
| }, | ||
| { | ||
| "name": "Grok Code Fast 1", | ||
| "consumption": "0x" | ||
| }, | ||
| { | ||
| "name": "Claude Haiku 4.5", | ||
| "consumption": "0.33x" | ||
| }, | ||
| { | ||
| "name": "Claude Sonnet 4", | ||
| "consumption": "1x" | ||
| }, | ||
| { | ||
| "name": "Claude Sonnet 4.5", | ||
| "consumption": "1x" | ||
| }, | ||
| { | ||
| "name": "Gemini 2.5 Pro", | ||
| "consumption": "1x" | ||
| }, | ||
| { | ||
| "name": "GPT-5", | ||
| "consumption": "1x" | ||
| }, | ||
| { | ||
| "name": "GPT-5-Codex (Preview)", | ||
| "consumption": "1x" | ||
| } | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,139 @@ | ||||||||||||||||||||||
| import consola from "consola" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import { state } from "./state" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Find a matching model from available models | ||||||||||||||||||||||
| * If exact match exists, return it | ||||||||||||||||||||||
| * If no exact match, try to find by prefix (e.g., claude-haiku-4-5-xxx -> claude-haiku-4.5) | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| export function findMatchingModel(requestedModel: string): string | null { | ||||||||||||||||||||||
| const availableModels = state.models?.data.filter( | ||||||||||||||||||||||
| (m) => typeof m.capabilities?.limits?.max_context_window_tokens === "number", | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (!availableModels || availableModels.length === 0) { | ||||||||||||||||||||||
| return null | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const availableModelIds = availableModels.map((m) => m.id) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| consola.debug(`Looking for match for: ${requestedModel}`) | ||||||||||||||||||||||
| consola.debug(`Available models: ${availableModelIds.join(", ")}`) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Try exact match first | ||||||||||||||||||||||
| if (availableModelIds.includes(requestedModel)) { | ||||||||||||||||||||||
| return requestedModel | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Normalize the requested model | ||||||||||||||||||||||
| // 1. Replace underscores with hyphens | ||||||||||||||||||||||
| // 2. Remove date suffix (8 digits at the end) | ||||||||||||||||||||||
| // 3. Replace version numbers: 4-5 -> 4.5 | ||||||||||||||||||||||
| let normalizedRequested = requestedModel | ||||||||||||||||||||||
| .toLowerCase() | ||||||||||||||||||||||
| .replace(/_/g, "-") | ||||||||||||||||||||||
| .replace(/-(\d{8})$/, "") // Remove -20251001 style suffix | ||||||||||||||||||||||
| .replace(/(\d)-(\d)/g, "$1.$2") // Replace 4-5 with 4.5 | ||||||||||||||||||||||
|
||||||||||||||||||||||
| .replace(/(\d)-(\d)/g, "$1.$2") // Replace 4-5 with 4.5 | |
| .replace(/(\d+)-(\d+)(?=\D|$)/g, "$1.$2") // Replace 4-5 with 4.5, but not 3-5 in gpt-3-5-turbo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normalization breaks valid IDs like gpt-4-1106-preview
replace(/(\d)-(\d)/g, "$1.$2") also fires on multi-digit suffixes, so a request for gpt-4-1106-preview becomes gpt-4.1106-preview and can no longer match the real model ID. This makes validateAndReplaceModel reject legitimate models. Please constrain the normalization to single-digit version fragments only, e.g.:
- .replace(/(\d)-(\d)/g, "$1.$2") // Replace 4-5 with 4.5
+ .replace(/\b(\d)-(\d)\b/g, (_match, major, minor) => `${major}.${minor}`) // Replace 4-5 with 4.5📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let normalizedRequested = requestedModel | |
| .toLowerCase() | |
| .replace(/_/g, "-") | |
| .replace(/-(\d{8})$/, "") // Remove -20251001 style suffix | |
| .replace(/(\d)-(\d)/g, "$1.$2") // Replace 4-5 with 4.5 | |
| let normalizedRequested = requestedModel | |
| .toLowerCase() | |
| .replace(/_/g, "-") | |
| .replace(/-(\d{8})$/, "") // Remove -20251001 style suffix | |
| .replace(/\b(\d)-(\d)\b/g, (_match, major, minor) => `${major}.${minor}`) // Replace 4-5 with 4.5 |
🧰 Tools
🪛 ESLint
[error] 33-33: 'normalizedRequested' is never reassigned. Use 'const' instead.
(prefer-const)
[error] 35-35: Prefer String#replaceAll() over String#replace().
(unicorn/prefer-string-replace-all)
[error] 36-36: Capturing group number 1 is defined but never used.
(regexp/no-unused-capturing-group)
[error] 37-37: Prefer String#replaceAll() over String#replace().
(unicorn/prefer-string-replace-all)
🤖 Prompt for AI Agents
In src/lib/model-matcher.ts around lines 33 to 38, the normalization step's
pattern that replaces digit-dash-digit sequences also matches multi-digit
fragments (e.g. transforms "gpt-4-1106-preview" to "gpt-4.1106-preview");
restrict that replacement so it only converts single-digit version fragments (a
single digit, a dash, a single digit) and does not fire when the digit after the
dash is followed by additional digits (i.e. ensure the second digit is not part
of a multi-digit sequence or use a word boundary), so multi-digit suffixes
remain unchanged.
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bidirectional prefix matching could produce ambiguous results when multiple models share prefixes. For example, if "gpt-4" is requested and both "gpt-4" and "gpt-4o" are available, this could match either one depending on iteration order. Consider matching only one direction or adding explicit preference logic.
| normalizedAvailable.startsWith(normalizedRequested) || | |
| normalizedRequested.startsWith(normalizedAvailable) | |
| normalizedAvailable.startsWith(normalizedRequested) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { Hono } from "hono" | |||||
| import { forwardError } from "~/lib/error" | ||||||
| import { state } from "~/lib/state" | ||||||
| import { cacheModels } from "~/lib/utils" | ||||||
| import modelConsumptionData from "~/lib/model-consumption.json" | ||||||
|
|
||||||
| export const modelRoutes = new Hono() | ||||||
|
|
||||||
|
|
@@ -13,15 +14,39 @@ modelRoutes.get("/", async (c) => { | |||||
| await cacheModels() | ||||||
| } | ||||||
|
|
||||||
| const models = state.models?.data.map((model) => ({ | ||||||
| id: model.id, | ||||||
| object: "model", | ||||||
| type: "model", | ||||||
| created: 0, // No date available from source | ||||||
| created_at: new Date(0).toISOString(), // No date available from source | ||||||
| owned_by: model.vendor, | ||||||
| display_name: model.name, | ||||||
| })) | ||||||
| // Create a map for quick consumption lookup | ||||||
| const consumptionMap = new Map( | ||||||
| modelConsumptionData.models.map((m) => [m.name, m.consumption]), | ||||||
| ) | ||||||
|
|
||||||
| // Helper function to convert consumption string to number for sorting | ||||||
| const consumptionToNumber = (consumption: string): number => { | ||||||
| if (consumption === "N/A") return 999 // Put N/A at the end | ||||||
| const match = consumption.match(/^([\d.]+)x$/) | ||||||
| return match ? Number.parseFloat(match[1]) : 999 | ||||||
| } | ||||||
|
Comment on lines
+17
to
+27
|
||||||
|
|
||||||
| // Filter to only include models with context window information (Available models) | ||||||
| const models = state.models?.data | ||||||
| .filter((model) => { | ||||||
| const maxTokens = model.capabilities?.limits?.max_context_window_tokens | ||||||
| return typeof maxTokens === "number" | ||||||
| }) | ||||||
| .map((model) => ({ | ||||||
| model, | ||||||
| consumption: consumptionMap.get(model.name) || "N/A", | ||||||
| })) | ||||||
| .sort((a, b) => consumptionToNumber(a.consumption) - consumptionToNumber(b.consumption)) | ||||||
| .map((item) => ({ | ||||||
| id: item.model.id, | ||||||
| object: "model", | ||||||
| type: "model", | ||||||
| created: 0, // No date available from source | ||||||
| created_at: new Date(0).toISOString(), // No date available from source | ||||||
| owned_by: item.model.vendor, | ||||||
| display_name: item.model.name, | ||||||
| max_context_length: item.model.capabilities?.limits?.max_context_window_tokens, | ||||||
|
||||||
| max_context_length: item.model.capabilities?.limits?.max_context_window_tokens, | |
| max_context_window_tokens: item.model.capabilities?.limits?.max_context_window_tokens, |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -9,7 +9,12 @@ export const getModels = async () => { | |||||
|
|
||||||
| if (!response.ok) throw new HTTPError("Failed to get models", response) | ||||||
|
|
||||||
| return (await response.json()) as ModelsResponse | ||||||
| const result = await response.json() as ModelsResponse | ||||||
| result.data = result.data.filter( | ||||||
| (model: any) => | ||||||
|
||||||
| (model: any) => | |
| (model: Model) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify model names match actual GitHub Copilot models.
The consumption data structure looks good, but some model names appear unusual (e.g., "GPT-5", "GPT-5 mini", "GPT-5-Codex (Preview)", "Grok Code Fast 1"). These may be placeholders or future models.
Please verify that these model names match actual GitHub Copilot model IDs by searching the web for the latest Copilot model documentation:
Minor note: The AI summary mentions 11 models, but the file contains 10 models.
🌐 Web query:
💡 Result:
Here are the GitHub Copilot models announced/available around November 2024 (names as published during the GitHub Universe Oct 2024 multi‑model announcement). Sources below.
Sources:
[1] The Verge — “GitHub Copilot will support models from Anthropic, Google, and OpenAI” (Oct 29, 2024).
[2] GitHub Universe coverage / press summaries reporting the Oct 2024 Copilot multi‑model announcement.
Critical: Update model names to match actual GitHub Copilot models available as of November 2024.
The current GitHub Copilot models announced are: Claude 3.5 Sonnet (Anthropic), Gemini 1.5 Pro (Google), and GPT-4o (OpenAI), with o1-preview and o1-mini variants also available.
Most names in this file are inaccurate: