Skip to content

Commit f2c2d7a

Browse files
Merge czy-all into dev
Resolve PR #39 conflicts by preserving dev-side quota and routing fixes while bringing in upstream Responses/Messages updates. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2 parents 8de95ec + 4e5854a commit f2c2d7a

36 files changed

Lines changed: 1627 additions & 908 deletions

desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "copilot-api-desktop",
3-
"version": "1.10.12",
3+
"version": "1.10.22",
44
"description": "Copilot API Desktop App",
55
"main": "out/main/index.js",
66
"scripts": {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/package.json",
33
"name": "@jeffreycao/copilot-api",
4-
"version": "1.10.12",
4+
"version": "1.10.22",
55
"description": "OpenAI and Anthropic-compatible gateway for GitHub Copilot or Codex or third-party providers.",
66
"keywords": [
77
"ai-gateway",

src/lib/api-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ const OPENCODE_VERSION = "opencode/1.14.29"
150150
const OPENCODE_LLM_USER_AGENT =
151151
"opencode/1.14.29 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13, opencode/1.14.29"
152152

153-
export const COPILOT_VERSION = "0.47.1"
153+
export const COPILOT_VERSION = "0.50.1"
154154
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`
155155
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`
156156
const CLAUDE_AGENT_USER_AGENT =

src/lib/models.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import type { Model } from "~/services/copilot/get-models"
22

33
import { state } from "~/lib/state"
44

5+
export interface NormalizedSdkModelId {
6+
family: string
7+
version: string
8+
}
9+
510
export const findEndpointModel = (sdkModelId: string): Model | undefined => {
611
const models = state.models?.data ?? []
712
const exactMatch = models.find((m) => m.id === sdkModelId)
813
if (exactMatch) {
914
return exactMatch
1015
}
1116

12-
const normalized = _normalizeSdkModelId(sdkModelId)
17+
const normalized = normalizeSdkModelId(sdkModelId)
1318
if (!normalized) {
1419
return undefined
1520
}
@@ -33,30 +38,30 @@ export const findEndpointModel = (sdkModelId: string): Model | undefined => {
3338
* - "claude-haiku-3-5-20250514" -> { family: "haiku", version: "3.5" }
3439
* - "claude-haiku-4.5" -> { family: "haiku", version: "4.5" }
3540
*/
36-
const _normalizeSdkModelId = (
41+
export const normalizeSdkModelId = (
3742
sdkModelId: string,
38-
): { family: string; version: string } | undefined => {
43+
): NormalizedSdkModelId | undefined => {
3944
const lower = sdkModelId.toLowerCase()
4045

4146
// Strip date suffix (8 digits at the end)
4247
const withoutDate = lower.replace(/-\d{8}$/, "")
4348

44-
// Pattern 1: claude-{family}-{major}-{minor} (e.g., claude-opus-4-5, claude-haiku-3-5)
45-
const pattern1 = withoutDate.match(/^claude-(\w+)-(\d+)-(\d+)$/)
49+
// Pattern 1: claude-{family}-{major}.{minor} (e.g., claude-haiku-4.5)
50+
const pattern1 = withoutDate.match(/^claude-(\w+)-(\d+)\.(\d+)$/)
4651
if (pattern1) {
4752
return { family: pattern1[1], version: `${pattern1[2]}.${pattern1[3]}` }
4853
}
4954

50-
// Pattern 2: claude-{major}-{minor}-{family} (e.g., claude-3-5-sonnet)
51-
const pattern2 = withoutDate.match(/^claude-(\d+)-(\d+)-(\w+)$/)
55+
// Pattern 2: claude-{family}-{major}-{minor} (e.g., claude-opus-4-5, claude-haiku-3-5)
56+
const pattern2 = withoutDate.match(/^claude-(\w+)-(\d+)-(\d+)$/)
5257
if (pattern2) {
53-
return { family: pattern2[3], version: `${pattern2[1]}.${pattern2[2]}` }
58+
return { family: pattern2[1], version: `${pattern2[2]}.${pattern2[3]}` }
5459
}
5560

56-
// Pattern 3: claude-{family}-{major}.{minor} (e.g., claude-haiku-4.5)
57-
const pattern3 = withoutDate.match(/^claude-(\w+)-(\d+)\.(\d+)$/)
61+
// Pattern 3: claude-{major}-{minor}-{family} (e.g., claude-3-5-sonnet)
62+
const pattern3 = withoutDate.match(/^claude-(\d+)-(\d+)-(\w+)$/)
5863
if (pattern3) {
59-
return { family: pattern3[1], version: `${pattern3[2]}.${pattern3[3]}` }
64+
return { family: pattern3[3], version: `${pattern3[1]}.${pattern3[2]}` }
6065
}
6166

6267
// Pattern 4: claude-{family}-{major} (e.g., claude-sonnet-4)

src/lib/utils.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,60 @@ export const sleep = (ms: number) =>
2020
export const isNullish = (value: unknown): value is null | undefined =>
2121
value === null || value === undefined
2222

23-
export async function cacheModels(): Promise<void> {
24-
const models = await getCopilotModels()
23+
// Periodically refresh models so long-running daemons pick up new SKUs.
24+
const MODELS_REFRESH_BASE_MS = 30 * 60 * 1000
25+
let modelsRefreshTimer: ReturnType<typeof setTimeout> | null = null
26+
27+
export const stopModelsRefreshLoop = () => {
28+
if (modelsRefreshTimer) {
29+
clearTimeout(modelsRefreshTimer)
30+
modelsRefreshTimer = null
31+
}
32+
}
2533

34+
type ModelsFetcher = typeof getCopilotModels
35+
36+
const refreshModels = async (fetcher: ModelsFetcher) => {
37+
const prevIds = new Set(state.models?.data.map((m) => m.id) ?? [])
38+
const models = await fetcher()
2639
state.models = {
2740
...models,
2841
data: models.data.filter((model) => model.model_picker_enabled),
2942
}
43+
const nextIds = state.models.data.map((m) => m.id)
44+
const added = nextIds.filter((id) => !prevIds.has(id))
45+
if (added.length > 0) {
46+
consola.info(`Models refresh: ${added.length} new -- ${added.join(", ")}`)
47+
} else {
48+
consola.debug(`Models refresh: no changes (${nextIds.length} total)`)
49+
}
50+
}
51+
52+
const scheduleModelsRefresh = (fetcher: ModelsFetcher, intervalMs: number) => {
53+
const jitter = Math.floor(Math.random() * (intervalMs / 6))
54+
const delay = intervalMs + jitter
55+
consola.debug(
56+
`Scheduling next models refresh in ${Math.round(delay / 1000)} seconds`,
57+
)
58+
59+
stopModelsRefreshLoop()
60+
modelsRefreshTimer = setTimeout(async () => {
61+
try {
62+
await refreshModels(fetcher)
63+
} catch (error) {
64+
consola.warn("Failed to refresh models, keeping previous cache.", error)
65+
} finally {
66+
scheduleModelsRefresh(fetcher, intervalMs)
67+
}
68+
}, delay)
69+
}
70+
71+
export async function cacheModels(
72+
fetcher: ModelsFetcher = getCopilotModels,
73+
intervalMs: number = MODELS_REFRESH_BASE_MS,
74+
): Promise<void> {
75+
await refreshModels(fetcher)
76+
scheduleModelsRefresh(fetcher, intervalMs)
3077
}
3178

3279
export const cacheVSCodeVersion = async () => {

src/routes/messages/anthropic-types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
export interface AnthropicMessagesPayload {
44
model: string
5-
messages: Array<AnthropicMessage>
5+
messages: Array<AnthropicInputMessage>
66
cache_control?: AnthropicCacheControl | null
77
system?: string | Array<AnthropicTextBlock>
88
stop_sequences?: Array<string>
@@ -125,7 +125,13 @@ export interface AnthropicAssistantMessage {
125125
content: string | Array<AnthropicAssistantContentBlock>
126126
}
127127

128+
export interface AnthropicSystemMessage {
129+
role: "system"
130+
content: string | Array<AnthropicTextBlock>
131+
}
132+
128133
export type AnthropicMessage = AnthropicUserMessage | AnthropicAssistantMessage
134+
export type AnthropicInputMessage = AnthropicMessage | AnthropicSystemMessage
129135

130136
export interface AnthropicTool {
131137
name: string

src/routes/messages/count-tokens-handler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { type Model } from "~/services/copilot/get-models"
1818
import { findEndpointModel } from "../../lib/models"
1919
import { type AnthropicMessagesPayload } from "./anthropic-types"
2020
import { translateToOpenAI } from "./non-stream-translation"
21+
import { normalizeSystemMessages } from "./preprocess"
2122

2223
export const resolveCountTokensModel = (
2324
modelId: string,
@@ -92,6 +93,7 @@ async function countTokensViaAnthropic(
9293
export async function handleCountTokens(c: Context) {
9394
const anthropicPayload = await c.req.json<AnthropicMessagesPayload>()
9495
anthropicPayload.model = resolveMappedModel(anthropicPayload.model)
96+
normalizeSystemMessages(anthropicPayload)
9597

9698
const providerModelAlias = parseProviderModelAlias(anthropicPayload.model)
9799
if (providerModelAlias) {

src/routes/messages/handler.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
getCompactType,
3838
getLastMessageContentCacheControl,
3939
mergeToolResultForClaude,
40+
normalizeSystemMessages,
4041
sanitizeIdeTools,
4142
stripToolReferenceTurnBoundary,
4243
} from "./preprocess"
@@ -70,10 +71,13 @@ export async function handleCompletion(c: Context) {
7071
})
7172
}
7273

74+
debugJson(logger, "Anthropic request payload:", anthropicPayload)
75+
76+
normalizeSystemMessages(anthropicPayload)
77+
7378
await checkRateLimit(state)
7479

7580
const originalModel = anthropicPayload.model
76-
debugJson(logger, "Anthropic request payload:", anthropicPayload)
7781

7882
const compactType = getCompactType(anthropicPayload)
7983
if (compactType !== 0) {

src/routes/messages/non-stream-translation.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
type AnthropicAssistantContentBlock,
1717
type AnthropicAssistantMessage,
1818
type AnthropicDocumentBlock,
19+
type AnthropicMessage,
1920
type AnthropicMessagesPayload,
2021
type AnthropicResponse,
2122
type AnthropicTextBlock,
@@ -124,10 +125,11 @@ function translateAnthropicMessagesToOpenAI(
124125
capabilities: TranslationCapabilities,
125126
): Array<Message> {
126127
const systemMessages = handleSystemPrompt(payload.system)
127-
const otherMessages = payload.messages.flatMap((message) =>
128-
message.role === "user" ?
129-
handleUserMessage(message, capabilities)
130-
: handleAssistantMessage(message, modelId, capabilities),
128+
const otherMessages = (payload.messages as Array<AnthropicMessage>).flatMap(
129+
(message) =>
130+
message.role === "user" ?
131+
handleUserMessage(message, capabilities)
132+
: handleAssistantMessage(message, modelId, capabilities),
131133
)
132134
return [...systemMessages, ...otherMessages]
133135
}

0 commit comments

Comments
 (0)