Skip to content

Commit 768f573

Browse files
kubaerorxuangongVincenthaysh4aiwonderyl
committed
fix: implement 5 bug fixes from upstream PRs
- Auto re-authenticate on 401 (PR ericc-ch#202) - Set response object type for non-streaming (PR ericc-ch#185) - Fix tool 400 error with missing properties (PR ericc-ch#192) - Fix model name translation regex (PR ericc-ch#180) - Filter Anthropic reserved keywords from system prompts (PR ericc-ch#175) Co-authored-by: xuangong <xuangong@users.noreply.github.com> Co-authored-by: Vincenthays <Vincenthays@users.noreply.github.com> Co-authored-by: h4ai <h4ai@users.noreply.github.com> Co-authored-by: wonderyl <wonderyl@users.noreply.github.com> Co-authored-by: qihonggang <qihonggang@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0ea08fe commit 768f573

File tree

3 files changed

+79
-22
lines changed

3 files changed

+79
-22
lines changed

src/lib/token.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")
1515
const writeGithubToken = (token: string) =>
1616
fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token)
1717

18+
const clearGithubToken = async () => {
19+
state.githubToken = undefined
20+
try {
21+
await fs.unlink(PATHS.GITHUB_TOKEN_PATH)
22+
} catch {
23+
// Ignore if file doesn't exist
24+
}
25+
}
26+
1827
export const setupCopilotToken = async () => {
1928
const { token, refresh_in } = await getCopilotToken()
2029
state.copilotToken = token
@@ -51,33 +60,49 @@ export async function setupGitHubToken(
5160
): Promise<void> {
5261
try {
5362
const githubToken = await readGithubToken()
63+
let needsReAuth = false
5464

5565
if (githubToken && !options?.force) {
5666
state.githubToken = githubToken
5767
if (state.showToken) {
5868
consola.info("GitHub token:", githubToken)
5969
}
60-
await logUser()
6170

62-
return
71+
// PR #202: Auto re-authenticate on 401 (@xuangong)
72+
// If the stored token is invalid, clear it and fall through to re-auth
73+
try {
74+
await logUser()
75+
return // Token is valid, we're done
76+
} catch (error) {
77+
if (error instanceof HTTPError && error.response.status === 401) {
78+
consola.warn("Stored GitHub token is invalid, re-authenticating...")
79+
await clearGithubToken()
80+
needsReAuth = true
81+
} else {
82+
throw error
83+
}
84+
}
6385
}
6486

65-
consola.info("Not logged in, getting new access token")
66-
const response = await getDeviceCode()
67-
consola.debug("Device code response:", response)
87+
if (!githubToken || options?.force || needsReAuth) {
88+
consola.info("Not logged in, getting new access token")
89+
const response = await getDeviceCode()
90+
consola.debug("Device code response:", response)
6891

69-
consola.info(
70-
`Please enter the code "${response.user_code}" in ${response.verification_uri}`,
71-
)
92+
consola.info(
93+
`Please enter the code "${response.user_code}" in ${response.verification_uri}`,
94+
)
7295

73-
const token = await pollAccessToken(response)
74-
await writeGithubToken(token)
75-
state.githubToken = token
96+
const newToken = await pollAccessToken(response)
97+
await writeGithubToken(newToken)
98+
// eslint-disable-next-line require-atomic-updates -- function is not called concurrently
99+
state.githubToken = newToken
76100

77-
if (state.showToken) {
78-
consola.info("GitHub token:", token)
101+
if (state.showToken) {
102+
consola.info("GitHub token:", newToken)
103+
}
104+
await logUser()
79105
}
80-
await logUser()
81106
} catch (error) {
82107
if (error instanceof HTTPError) {
83108
consola.error("Failed to get GitHub token:", await error.response.json())

src/routes/chat-completions/handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ export async function handleCompletion(c: Context) {
5151

5252
if (isNonStreaming(response)) {
5353
consola.debug("Non-streaming response:", JSON.stringify(response))
54-
return c.json(response)
54+
// Add object type for pydantic_ai compatibility (PR #185 by @Vincenthays)
55+
return c.json({ ...response, object: "chat.completion" })
5556
}
5657

5758
consola.debug("Streaming response")

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

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,16 @@ export function translateToOpenAI(
4747
}
4848

4949
function translateModelName(model: string): string {
50+
// PR #180: Fix model name translation (@wonderyl)
5051
// Subagent requests use a specific model number which Copilot doesn't support
51-
if (model.startsWith("claude-sonnet-4-")) {
52-
return model.replace(/^claude-sonnet-4-.*/, "claude-sonnet-4")
53-
} else if (model.startsWith("claude-opus-")) {
54-
return model.replace(/^claude-opus-4-.*/, "claude-opus-4")
52+
// Match patterns like claude-sonnet-4-5-20250929 or claude-sonnet-4-20250929
53+
if (model.startsWith("claude-sonnet-4")) {
54+
return model.replace(
55+
/^claude-sonnet-4(?:-\d+)?(?:-\d{8})?$/,
56+
"claude-sonnet-4",
57+
)
58+
} else if (model.startsWith("claude-opus-4")) {
59+
return model.replace(/^claude-opus-4(?:-\d+)?(?:-\d{8})?$/, "claude-opus-4")
5560
}
5661
return model
5762
}
@@ -71,6 +76,21 @@ function translateAnthropicMessagesToOpenAI(
7176
return [...systemMessages, ...otherMessages]
7277
}
7378

79+
// PR #175: Filter Anthropic reserved keywords (@qihonggang)
80+
// Filter out Anthropic-specific billing headers from system prompts
81+
const ANTHROPIC_RESERVED_KEYWORDS = [
82+
"x-anthropic-billing-header",
83+
"x-anthropic-billing",
84+
]
85+
86+
function filterAnthropicKeywords(text: string): string {
87+
let filtered = text
88+
for (const keyword of ANTHROPIC_RESERVED_KEYWORDS) {
89+
filtered = filtered.replaceAll(new RegExp(keyword, "gi"), "")
90+
}
91+
return filtered
92+
}
93+
7494
function handleSystemPrompt(
7595
system: string | Array<AnthropicTextBlock> | undefined,
7696
): Array<Message> {
@@ -79,10 +99,10 @@ function handleSystemPrompt(
7999
}
80100

81101
if (typeof system === "string") {
82-
return [{ role: "system", content: system }]
102+
return [{ role: "system", content: filterAnthropicKeywords(system) }]
83103
} else {
84104
const systemText = system.map((block) => block.text).join("\n\n")
85-
return [{ role: "system", content: systemText }]
105+
return [{ role: "system", content: filterAnthropicKeywords(systemText) }]
86106
}
87107
}
88108

@@ -228,6 +248,17 @@ function mapContent(
228248
return contentParts
229249
}
230250

251+
// PR #192: Fix tool 400 error (@h4ai)
252+
// Ensure object schemas always have a properties field to avoid Copilot API errors
253+
function normalizeToolSchema(
254+
schema: Record<string, unknown>,
255+
): Record<string, unknown> {
256+
if (schema.type === "object" && !schema.properties) {
257+
return { ...schema, properties: {} }
258+
}
259+
return schema
260+
}
261+
231262
function translateAnthropicToolsToOpenAI(
232263
anthropicTools: Array<AnthropicTool> | undefined,
233264
): Array<Tool> | undefined {
@@ -239,7 +270,7 @@ function translateAnthropicToolsToOpenAI(
239270
function: {
240271
name: tool.name,
241272
description: tool.description,
242-
parameters: tool.input_schema,
273+
parameters: normalizeToolSchema(tool.input_schema),
243274
},
244275
}))
245276
}

0 commit comments

Comments
 (0)