merge main (0.6.0) into fix/omp-developer-messages
Align the developer-message fix with the Provider API transport router. The fix stays scoped to the legacy /alpha/generate converter, and the OMP advisory compat test now forces the upgrade_required fallback so the advisory is asserted on the generate request body.
This commit is contained in:
+314
-3
@@ -6,6 +6,7 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { after, before, beforeEach, describe, it } from "node:test"
|
||||
|
||||
import { COMMAND_CODE_CLI_VERSION } from "../src/commandcode-catalog.ts"
|
||||
import type { AssistantMessageEvent } from "../src/core.ts"
|
||||
import { MODEL_EFFORTS, thinkingLevelMapForEfforts } from "../src/models.ts"
|
||||
import {
|
||||
@@ -76,6 +77,23 @@ describe("streamCommandCode — auth", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("accepts the official CLI API key environment variable", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({
|
||||
apiBase: server.baseUrl(),
|
||||
env: { COMMAND_CODE_API_KEY: "official-env-key" },
|
||||
})
|
||||
|
||||
await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), { apiKey: "$COMMAND_CODE_API_KEY" }),
|
||||
)
|
||||
|
||||
assert.equal(server.lastRequestHeaders().authorization, "Bearer official-env-key")
|
||||
})
|
||||
|
||||
it("uses options.apiKey in the Authorization header", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
@@ -173,6 +191,101 @@ describe("streamCommandCode — successful streams", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("forwards a tool-result image as a following user image for vision-capable models", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
const events = await collectEvents(
|
||||
streamCommandCode(
|
||||
makeModel({ id: "deepseek/deepseek-v4-flash-vision-exp" }),
|
||||
makeContext({
|
||||
messages: [
|
||||
{ role: "user", content: "read the image" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "c1", name: "read", arguments: {} }],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "c1",
|
||||
toolName: "read",
|
||||
content: [
|
||||
{ type: "text", text: "image attached" },
|
||||
{ type: "image", data: "aGVsbG8=", mimeType: "image/png" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ apiKey: "mock-key" },
|
||||
),
|
||||
)
|
||||
|
||||
// No error: the tool-result image must not be rejected for this model.
|
||||
assert.equal(events.at(-1)?.type, "done")
|
||||
|
||||
const body = server.lastRequestBody()
|
||||
// The tool-result text is forwarded on the tool message at index 2.
|
||||
assert.equal(
|
||||
objectAt(body, ["params", "messages", "2", "content", "0", "output", "value"]),
|
||||
"image attached",
|
||||
)
|
||||
// The tool-result image is forwarded as a following user image message at index 3.
|
||||
assert.equal(objectAt(body, ["params", "messages", "3", "role"]), "user")
|
||||
assert.equal(
|
||||
objectAt(body, ["params", "messages", "3", "content", "0", "image"]),
|
||||
"data:image/png;base64,aGVsbG8=",
|
||||
)
|
||||
})
|
||||
|
||||
it("omits a historical tool-result image after switching to a text-only model", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
const events = await collectEvents(
|
||||
streamCommandCode(
|
||||
makeModel({ id: "deepseek/deepseek-v4-flash" }),
|
||||
makeContext({
|
||||
messages: [
|
||||
{ role: "user", content: "read the image" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "c1", name: "read", arguments: {} }],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "c1",
|
||||
toolName: "read",
|
||||
content: [
|
||||
{ type: "text", text: "image attached" },
|
||||
{ type: "image", data: "aGVsbG8=", mimeType: "image/png" },
|
||||
],
|
||||
},
|
||||
{ role: "user", content: "continue without the image" },
|
||||
],
|
||||
}),
|
||||
{ apiKey: "mock-key" },
|
||||
),
|
||||
)
|
||||
|
||||
assert.equal(events.at(-1)?.type, "done")
|
||||
assert.equal(server.requestCount(), 1)
|
||||
const body = server.lastRequestBody()
|
||||
assert.equal(
|
||||
objectAt(body, ["params", "messages", "2", "content", "0", "output", "value"]),
|
||||
"image attached",
|
||||
)
|
||||
assert.equal(
|
||||
objectAt(body, ["params", "messages", "3", "content"]),
|
||||
"continue without the image",
|
||||
)
|
||||
})
|
||||
|
||||
it("rejects images before network access for text-only models", async () => {
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
@@ -326,6 +439,104 @@ describe("streamCommandCode — successful streams", () => {
|
||||
assert.equal(toolCall?.type === "toolCall" ? toolCall.name : "", "read_file")
|
||||
})
|
||||
|
||||
it("streams incremental tool-call arguments from generate events", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [
|
||||
JSON.stringify({
|
||||
type: "tool-input-start",
|
||||
id: "call_1",
|
||||
toolName: "read_file",
|
||||
}),
|
||||
JSON.stringify({ type: "tool-input-delta", id: "call_1", delta: '{"path":"' }),
|
||||
JSON.stringify({ type: "tool-input-delta", id: "call_1", delta: '/tmp/x"}' }),
|
||||
JSON.stringify({ type: "tool-input-end", id: "call_1" }),
|
||||
JSON.stringify({
|
||||
type: "tool-call",
|
||||
toolCallId: "call_1",
|
||||
toolName: "read_file",
|
||||
input: { path: "/tmp/x" },
|
||||
}),
|
||||
JSON.stringify({ type: "finish", finishReason: "tool-calls" }),
|
||||
],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
const events = await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||
)
|
||||
|
||||
assert.deepEqual(eventTypes(events), [
|
||||
"start",
|
||||
"toolcall_start",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_end",
|
||||
"done",
|
||||
])
|
||||
const deltas = events.flatMap((event) => (event.type === "toolcall_delta" ? [event.delta] : []))
|
||||
assert.deepEqual(deltas, ['{"path":"', '/tmp/x"}'])
|
||||
|
||||
const done = events.at(-1)
|
||||
if (done?.type !== "done") throw new Error("expected done")
|
||||
assert.equal(done.reason, "toolUse")
|
||||
const toolCall = done.message.content[0]
|
||||
assert.equal(toolCall?.type, "toolCall")
|
||||
if (toolCall?.type !== "toolCall") throw new Error("expected tool call")
|
||||
assert.equal(toolCall.id, "call_1")
|
||||
assert.equal(toolCall.name, "read_file")
|
||||
assert.deepEqual(toolCall.arguments, { path: "/tmp/x" })
|
||||
})
|
||||
|
||||
it("keeps concurrent incremental tool calls separate", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [
|
||||
JSON.stringify({ type: "tool-input-start", id: "call_1", toolName: "read_file" }),
|
||||
JSON.stringify({ type: "tool-input-start", id: "call_2", toolName: "read_file" }),
|
||||
JSON.stringify({ type: "tool-input-delta", id: "call_1", delta: '{"path":"/a"}' }),
|
||||
JSON.stringify({ type: "tool-input-delta", id: "call_2", delta: '{"path":"/b"}' }),
|
||||
JSON.stringify({
|
||||
type: "tool-call",
|
||||
toolCallId: "call_2",
|
||||
toolName: "read_file",
|
||||
input: { path: "/b" },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "tool-call",
|
||||
toolCallId: "call_1",
|
||||
toolName: "read_file",
|
||||
input: { path: "/a" },
|
||||
}),
|
||||
JSON.stringify({ type: "finish", finishReason: "tool-calls" }),
|
||||
],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
const events = await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||
)
|
||||
|
||||
const starts = events.flatMap((event) =>
|
||||
event.type === "toolcall_start" ? [event.contentIndex] : [],
|
||||
)
|
||||
const deltas = events.flatMap((event) =>
|
||||
event.type === "toolcall_delta" ? [[event.contentIndex, event.delta] as const] : [],
|
||||
)
|
||||
const ends = events.flatMap((event) =>
|
||||
event.type === "toolcall_end" ? [[event.contentIndex, event.toolCall.id] as const] : [],
|
||||
)
|
||||
assert.deepEqual(starts, [0, 1])
|
||||
assert.deepEqual(deltas, [
|
||||
[0, '{"path":"/a"}'],
|
||||
[1, '{"path":"/b"}'],
|
||||
])
|
||||
assert.deepEqual(ends, [
|
||||
[1, "call_2"],
|
||||
[0, "call_1"],
|
||||
])
|
||||
})
|
||||
|
||||
it("flushes reasoning if finish arrives without reasoning-end", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
@@ -473,7 +684,7 @@ describe("streamCommandCode — request serialization", () => {
|
||||
assert.equal(objectAt(body, ["params", "stream"]), true)
|
||||
assert.equal(objectAt(body, ["params", "max_tokens"]), 500)
|
||||
assert.equal(objectAt(body, ["params", "reasoning_effort"]), undefined)
|
||||
assert.equal(objectAt(body, ["params", "temperature"]), 0.3)
|
||||
assert.equal(objectAt(body, ["params", "temperature"]), undefined)
|
||||
assert.equal(objectAt(body, ["params", "system"]), "You are a test assistant.")
|
||||
assert.equal(objectAt(body, ["memory"]), null)
|
||||
assert.equal(objectAt(body, ["taste"]), null)
|
||||
@@ -488,10 +699,11 @@ describe("streamCommandCode — request serialization", () => {
|
||||
|
||||
const headers = server.lastRequestHeaders()
|
||||
assert.equal(headers.authorization, "Bearer mock-key")
|
||||
assert.equal(headers["x-command-code-version"], "1.15.1")
|
||||
assert.equal(headers["x-command-code-version"], COMMAND_CODE_CLI_VERSION)
|
||||
assert.equal(headers["x-project-slug"], "repo")
|
||||
assert.equal(headers["x-taste-learning"], "true")
|
||||
assert.equal(headers["x-co-flag"], "false")
|
||||
assert.equal(headers["user-agent"], "cli")
|
||||
assert.equal(headers["x-co-flag"], undefined)
|
||||
assert.equal(headers["x-session-id"], undefined)
|
||||
})
|
||||
|
||||
@@ -545,6 +757,48 @@ describe("streamCommandCode — request serialization", () => {
|
||||
assert.doesNotMatch(String(objectAt(body, ["params", "system"])), /advisory/)
|
||||
})
|
||||
|
||||
it("forwards explicit temperature and stable session metadata", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), {
|
||||
apiKey: "mock-key",
|
||||
temperature: 0.7,
|
||||
sessionId: "11111111-1111-4111-8111-111111111111",
|
||||
}),
|
||||
)
|
||||
|
||||
const body = server.lastRequestBody()
|
||||
assert.equal(objectAt(body, ["params", "temperature"]), 0.7)
|
||||
assert.equal(objectAt(body, ["threadId"]), "11111111-1111-4111-8111-111111111111")
|
||||
assert.equal(
|
||||
server.lastRequestHeaders()["x-session-id"],
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
)
|
||||
})
|
||||
|
||||
it("omits non-UUID session ids from the generate thread id", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), {
|
||||
apiKey: "mock-key",
|
||||
sessionId: "human-readable-session",
|
||||
}),
|
||||
)
|
||||
|
||||
assert.equal(objectAt(server.lastRequestBody(), ["threadId"]), undefined)
|
||||
assert.equal(server.lastRequestHeaders()["x-session-id"], "human-readable-session")
|
||||
})
|
||||
|
||||
it("accepts the legacy OMP nested reasoning map", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
@@ -787,6 +1041,63 @@ describe("streamCommandCode — upstream errors and malformed streams", () => {
|
||||
assert.equal(error.error.errorMessage, "provider failed")
|
||||
})
|
||||
|
||||
it("rejects a truncated stream without a finish event", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "text-delta", text: "truncated" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
const events = await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||
)
|
||||
|
||||
const error = events.at(-1)
|
||||
assert.equal(error?.type, "error")
|
||||
if (error?.type !== "error") throw new Error("expected error")
|
||||
assert.match(error.error.errorMessage ?? "", /no finish event/i)
|
||||
})
|
||||
|
||||
it("maps an upstream abort event to an aborted request", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "abort" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
const events = await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||
)
|
||||
|
||||
const error = events.at(-1)
|
||||
assert.equal(error?.type, "error")
|
||||
if (error?.type !== "error") throw new Error("expected error")
|
||||
assert.equal(error.reason, "aborted")
|
||||
})
|
||||
|
||||
it("rejects terminal upstream network failure reasons", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [
|
||||
JSON.stringify({
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
rawFinishReason: "upstream_error",
|
||||
}),
|
||||
],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
const events = await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||
)
|
||||
|
||||
const error = events.at(-1)
|
||||
assert.equal(error?.type, "error")
|
||||
if (error?.type !== "error") throw new Error("expected error")
|
||||
assert.match(error.error.errorMessage ?? "", /upstream connection failed/i)
|
||||
})
|
||||
|
||||
it("handles SSE lines, malformed lines, split chunks, and final line without newline", async () => {
|
||||
const textEvent = `data: ${JSON.stringify({ type: "text-delta", text: "split" })}\n`
|
||||
const finishEvent = JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user