Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/joycode/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# 更新记录

## 0.2.2

- 根据真实网关支持值修复 Codex 压缩的推理强度:GPT-6 Astra、GPT-6.1 Sol 的 `none` / `minimal` 转为 `low`;GPT-6 Sol、GPT-5.6 Sol 的 `minimal` 转为 `low`,保留这两个模型支持的 `none`。其他模型、协议、有效强度和未指定强度不变。
- 2026-10-30 对账号目录的全部四个 Responses 模型逐一发送 `none` 和 `minimal` 共八次真实请求:两个 Sol 的 `none` 返回 `OK`;其余六个组合明确返回 `unsupported_value`,参数为 `reasoning.effort`,支持值均包含 `low`。不依据 Codex 模型目录推断 JoyCode 后端能力。

修补后八个组合再次通过插件进行真实流式请求,全部返回 `OK` 和 `response.completed`(1.3–4.7 秒)。

验证范围为短请求和插件转发链路,尚未在原始长会话中重跑 Codex 压缩。

## 0.2.1

- 修复 Codex 压缩时重放 `store: false` 的 `rs_…` reasoning id 被 JoyCode Responses 后端拒绝并连续失败的问题。转发前移除 reasoning item 的持久化 id,保留其摘要和加密内容;`store: true` 及其他 item 的 id 不变。
Expand Down
2 changes: 2 additions & 0 deletions packages/joycode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ magpie plugin login joycode

## 消息转发策略

根据 2026-10-30 的 JoyCode 实测,仅对 Responses 协议做以下兼容:GPT-6 Astra、GPT-6.1 Sol 的显式 `none` / `minimal` 转为 `low`;GPT-6 Sol、GPT-5.6 Sol 仅将 `minimal` 转为 `low`,保留上游接受的 `none`。其他 reasoning 参数、有效或未指定的强度、未知模型和其他协议均不变。启用低档推理可能增加用量。此表基于当前账号目录,不自动覆盖未来新增模型。

默认保留完整多轮对话、工具调用和模型原生请求格式。插件从当前账号的 `joycode_userInfo` 获取真实 `userId`、`tenant`、`orgFullName`,按 JoyCode 请求格式补入正文;三种协议统一设置 `language: UNKNOWN`。身份信息按账号 loader 隔离缓存,不接受客户端指定的用户身份。缺少这些元数据时,已观察到网关返回 `AI_GRAY_ACCESS_DENIED`。

网关可能以 HTTP 200 和 SSE 响应头返回裸 JSON 错误;插件会把这种错误转换为明确的失败响应,避免客户端一直等待。普通 403 权限错误不会再被误标为登录失效。
Expand Down
136 changes: 136 additions & 0 deletions packages/joycode/chat.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,142 @@ test('Request input carries its URL, body, method, and headers into routing', as
assert.equal(await r.text(), 'event: message_stop\n\ndata: {}\n\n')
})

test('Responses maps GPT-6 Astra reasoning none to low while preserving the payload', async t => {
let sent
const payload = {
model: 'GPT-6 Astra',
input: 'OK',
reasoning: {
effort: 'none',
summary: 'auto',
},
metadata: { requestId: 'keep-this-field' },
stream: true,
}
const r = await request(t, new Response('data: [DONE]\n\n', {
headers: { 'content-type': 'text/event-stream' },
}), {
input: 'https://api-ai.jd.com/v1/responses',
payload,
upstream(_url, init) { sent = JSON.parse(init.body) },
})
assert.equal(r.status, 200)
await r.text()
const { userId, tenant, orgFullName, language, ...forwarded } = sent
assert.deepEqual(forwarded, {
...payload,
reasoning: { ...payload.reasoning, effort: 'low' },
})
})

for (const [label, model, effort, expectedEffort] of [
['GPT-6 Astra none', 'GPT-6 Astra', 'none', 'low'],
['GPT-6 Astra minimal', 'GPT-6 Astra', 'minimal', 'low'],
['GPT-6.1 Sol none', 'GPT-6.1 Sol', 'none', 'low'],
['GPT-6.1 Sol minimal', 'GPT-6.1 Sol', 'minimal', 'low'],
['GPT-6 Sol minimal', 'GPT-6 Sol', 'minimal', 'low'],
['GPT-5.6 Sol minimal', 'GPT-5.6 Sol', 'minimal', 'low'],
['GPT-6 Sol none', 'GPT-6 Sol', 'none', 'none'],
['GPT-5.6 Sol none', 'GPT-5.6 Sol', 'none', 'none'],
['unknown model none', 'Unknown Model', 'none', 'none'],
['unknown model minimal', 'Unknown Model', 'minimal', 'minimal'],
]) {
test(`Responses maps ${label} reasoning effort`, async t => {
let sent
const payload = {
model,
input: 'OK',
reasoning: { effort, summary: 'auto' },
metadata: { case: label },
stream: true,
}
const r = await request(t, new Response('data: [DONE]\n\n', {
headers: { 'content-type': 'text/event-stream' },
}), {
input: 'https://api-ai.jd.com/v1/responses',
payload,
upstream(_url, init) { sent = JSON.parse(init.body) },
})
assert.equal(r.status, 200)
await r.text()
const { userId, tenant, orgFullName, language, ...forwarded } = sent
assert.deepEqual(forwarded, {
...payload,
reasoning: { ...payload.reasoning, effort: expectedEffort },
})
})
}

for (const [label, model, reasoning] of [
['low', 'GPT-6 Astra', { effort: 'low', summary: 'auto' }],
['medium', 'GPT-6 Astra', { effort: 'medium', summary: 'auto' }],
['high', 'GPT-6 Astra', { effort: 'high', summary: 'auto' }],
['xhigh', 'GPT-6 Astra', { effort: 'xhigh', summary: 'auto' }],
['max', 'GPT-6 Astra', { effort: 'max', summary: 'auto' }],
['missing effort', 'GPT-6 Astra', { summary: 'auto' }],
['other model none', 'GPT-5.6 Sol', { effort: 'none', summary: 'auto' }],
]) {
test(`Responses preserves reasoning effort ${label}`, async t => {
let sent
const payload = {
model,
input: 'OK',
reasoning,
metadata: { case: label },
stream: true,
}
const r = await request(t, new Response('data: [DONE]\n\n', {
headers: { 'content-type': 'text/event-stream' },
}), {
input: 'https://api-ai.jd.com/v1/responses',
payload,
upstream(_url, init) { sent = JSON.parse(init.body) },
})
assert.equal(r.status, 200)
await r.text()
const { userId, tenant, orgFullName, language, ...forwarded } = sent
assert.deepEqual(forwarded, payload)
})
}

for (const [name, input, payload] of [
[
'Chat Completions',
'https://api-ai.jd.com/v1/chat/completions',
{
model: 'GPT-6 Astra',
messages: [{ role: 'user', content: 'OK' }],
reasoning: { effort: 'none', summary: 'auto' },
stream: true,
},
],
[
'Anthropic Messages',
'https://api-ai.jd.com/v1/messages',
{
model: 'GPT-6 Astra',
max_tokens: 8,
messages: [{ role: 'user', content: 'OK' }],
reasoning: { effort: 'none', summary: 'auto' },
},
],
]) {
test(`${name} preserves GPT-6 Astra reasoning none`, async t => {
let sent
const r = await request(t, new Response('data: [DONE]\n\n', {
headers: { 'content-type': 'text/event-stream' },
}), {
input,
payload,
upstream(_url, init) { sent = JSON.parse(init.body) },
})
assert.equal(r.status, 200)
await r.text()
const { userId, tenant, orgFullName, language, ...forwarded } = sent
assert.deepEqual(forwarded, payload)
})
}

test('Anthropic double-wrapped SSE is normalized across chunk boundaries', async t => {
const upstreamText = [
'data: event: message_start\n\n',
Expand Down
12 changes: 12 additions & 0 deletions packages/joycode/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ const SDK = {
}
const LOGIN_TYPE = 'PIN_JD_CLOUD'

// Verified against JoyCode's live gateway, not the Codex model catalogue.
// GPT-6 Sol and GPT-5.6 Sol accept none; only minimal needs changing there.
const LOW_EFFORT_FALLBACKS = new Map([
['GPT-6 Astra', ['none', 'minimal']],
['GPT-6.1 Sol', ['none', 'minimal']],
['GPT-6 Sol', ['minimal']],
['GPT-5.6 Sol', ['minimal']],
])

// HMAC signing key, extracted from the JoyCode IDE's bundled extension.js.
//
// This is a PUBLIC PROTOCOL CONSTANT, not a secret: it is baked into every
Expand Down Expand Up @@ -774,6 +783,9 @@ export const JoyCodeAuthPlugin = async (_input, options) => {
: new TextDecoder().decode(source)
const payload = JSON.parse(source)
const protocol = requestProtocol(request ?? input, payload)
if (protocol === 'responses' && LOW_EFFORT_FALLBACKS.get(payload.model)?.includes(payload.reasoning?.effort)) {
payload.reasoning = { ...payload.reasoning, effort: 'low' }
}
const forwarded = protocol === 'responses' ? withoutUnstoredReasoningIds(payload) : payload
const body = JSON.stringify({
...(flatten && protocol === 'chat' ? flattenBody(payload) : forwarded),
Expand Down
2 changes: 1 addition & 1 deletion packages/joycode/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magpie-community/opencode-joycode-auth",
"version": "0.2.1",
"version": "0.2.2",
"description": "OpenCode provider plugin: JoyCode (JD Cloud JoyCoder IDE credits)",
"type": "module",
"main": "./index.mjs",
Expand Down
Loading