mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 23:22:09 +03:00
Compare commits
3 Commits
fix/10265-
...
fix/sec-ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b1d786bd1 | ||
|
|
6cd4d38e21 | ||
|
|
b6412c6fed |
1
changelog.d/fixes/10265-command-code-provider-api.md
Normal file
1
changelog.d/fixes/10265-command-code-provider-api.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(command-code): route chat to the documented /provider/v1/chat/completions endpoint instead of the CLI-only /alpha/generate, which Command Code gates/blocks for external callers (#10265)
|
||||
@@ -443,7 +443,7 @@
|
||||
"src/shared/components/ModelSelectModal.tsx": 1138,
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1250
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1073,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1080,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051,
|
||||
"src/shared/components/ModelSelectModal.tsx": 1138,
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1298,
|
||||
@@ -459,7 +459,8 @@
|
||||
"open-sse/executors/commandCode.ts": 1059,
|
||||
"_rebaseline_2026_08_21_10859_vision_bridge_catalog": "#10859 own growth (Vision Bridge fixes #10808/#10809): src/lib/modelCapabilities.ts 1006->1016 (+10, cmd/gpt-5.3-codex* text-only capability resolution) and open-sse/executors/commandCode.ts 988->1023 (+35, Command Code wire-model normalization for bare ids + reasoning field fallback for opencode-routed gateways). Cohesive bug fixes at the existing capability-resolution / executor chokepoints; not extractable mid-fix. Covered by tests/unit/model-capabilities-command-code-codex-textonly-10703.test.ts, tests/unit/command-code-vision.test.ts, tests/unit/opencode-mimo-reasoning-details-nonstream.test.ts. Pushed directly to release (own-session miss: the original rebaseline was made in a throwaway validation worktree and never landed on the PR branch or the release before merge).",
|
||||
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts.",
|
||||
"_rebaseline_2026_08_21_10986_reasoning_only_content": "#10986 own growth: open-sse/executors/commandCode.ts 1038->1059 (+21, reasoning-only content fallback — when upstream emits only reasoning-delta events and never a text-delta, surface the reasoning text as message.content in createJsonResponse and emit a synthetic content delta in createStreamResponse). Cohesive bug fix at the existing executor chokepoint (mirrors precedent style of #10907/#10859). Covered by tests/unit/command-code-executor.test.ts (2 new cases: non-stream + streaming)."
|
||||
"_rebaseline_2026_08_21_10986_reasoning_only_content": "#10986 own growth: open-sse/executors/commandCode.ts 1038->1059 (+21, reasoning-only content fallback — when upstream emits only reasoning-delta events and never a text-delta, surface the reasoning text as message.content in createJsonResponse and emit a synthetic content delta in createStreamResponse). Cohesive bug fix at the existing executor chokepoint (mirrors precedent style of #10907/#10859). Covered by tests/unit/command-code-executor.test.ts (2 new cases: non-stream + streaming).",
|
||||
"_rebaseline_2026_08_21_11069_m365_har_import": "#11069 own growth: AddApiKeyModal.tsx 1073->1080 (+7 = Import .har file button for the copilot-m365-web credential modal — M365 is the only provider whose credential (access_token+chathubPath) must be extracted from a DevTools HAR WebSocket URL, added as a new modal affordance). Cohesive UI at the existing modal chokepoint; not extractable. Covered by tests/unit/m365-har-import*.test.ts."
|
||||
},
|
||||
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
|
||||
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
|
||||
|
||||
@@ -8,7 +8,11 @@ export const command_codeProvider: RegistryEntry = {
|
||||
format: "openai",
|
||||
executor: "command-code",
|
||||
baseUrl: "https://api.commandcode.ai",
|
||||
chatPath: "/alpha/generate",
|
||||
// Chat uses the documented /provider/v1/chat/completions (OpenAI-format)
|
||||
// endpoint — NOT the CLI-only /alpha/generate endpoint, which Command Code
|
||||
// version-gates and proxy-blocks for external callers (#10265). Discovery
|
||||
// already targets the sibling /provider/v1/models endpoint.
|
||||
chatPath: "/provider/v1/chat/completions",
|
||||
modelsUrl: "https://api.commandcode.ai/provider/v1/models",
|
||||
// The discovery response is a partial routing catalog; static registry
|
||||
// entries omitted from it can still be accepted by the gateway.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,17 +30,45 @@ export const HANDSHAKE_REQUEST = { protocol: "json", version: 1 } as const;
|
||||
export const KEEPALIVE_PING = { type: 6 } as const;
|
||||
|
||||
/**
|
||||
* Allowed message types observed in the 2026-08 recapture of the working
|
||||
* `m365.cloud.microsoft/chat` client (#10718). The old 11-entry list is no longer
|
||||
* seen on the wire — the stale shape gets closed immediately after the type:4.
|
||||
* Allowed message types observed in a 2026-08-21 live capture of a working
|
||||
* `m365.cloud.microsoft/chat` session (issue: "Stream ended before producing a
|
||||
* non-ping SSE event" on every individual/consumer M365 Copilot call). The
|
||||
* #10718 6-entry shape above no longer produces a `type:1 target:"update"`
|
||||
* frame at all — the socket only replies with SignalR keepalive pings and then
|
||||
* closes, which is exactly what surfaces client-side as that generic stream
|
||||
* error. 30 entries, up from 6.
|
||||
*/
|
||||
export const ALLOWED_MESSAGE_TYPES = [
|
||||
"Chat",
|
||||
"Suggestion",
|
||||
"InternalSearchQuery",
|
||||
"Disengaged",
|
||||
"Progress",
|
||||
"EndOfRequest",
|
||||
"InternalLoaderMessage",
|
||||
"Progress",
|
||||
"GeneratedCode",
|
||||
"RenderCardRequest",
|
||||
"AdsQuery",
|
||||
"SemanticSerp",
|
||||
"GenerateContentQuery",
|
||||
"GenerateGraphicArt",
|
||||
"SearchQuery",
|
||||
"ConfirmationCard",
|
||||
"AuthError",
|
||||
"DeveloperLogs",
|
||||
"TriggerPlugin",
|
||||
"HintInvocation",
|
||||
"MemoryUpdate",
|
||||
"EndOfRequest",
|
||||
"TriggerConfirmation",
|
||||
"ResumeInvokeAction",
|
||||
"ResumeUserInputRequest",
|
||||
"TriggerUserInputRequest",
|
||||
"EscapeHatch",
|
||||
"TriggerPluginAuth",
|
||||
"ResumePluginAuth",
|
||||
"SideBySide",
|
||||
"ReferencesListComplete",
|
||||
"SwitchRespondingEndpoint",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@@ -78,19 +106,26 @@ export const M365_ENTERPRISE_EXTRA_MESSAGE_TYPES = [
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Individual / EDU option sets from the 2026-08 recapture (#10718) — 14 entries.
|
||||
* The previous 25-entry consumer/MSA set (enable_msa_user, pdnascan, cwc_code_*,
|
||||
* …) is no longer observed on the wire and belongs to the shape the substrate
|
||||
* now drops silently.
|
||||
* Individual / EDU option sets from a 2026-08-21 live capture — 34 entries, up
|
||||
* from the #10718 14-entry shape (which itself superseded an earlier 25-entry
|
||||
* shape). Each recapture so far has been additive/reshuffled rather than a
|
||||
* wholesale replacement — treat this as the protocol continuing to drift, not
|
||||
* a one-time fix; a future capture may again need to update this list.
|
||||
*/
|
||||
export const M365_DEFAULT_OPTION_SETS = [
|
||||
"search_result_progress_messages_with_search_queries",
|
||||
"update_textdoc_response_after_streaming",
|
||||
"deepleo_networking_timeout_10minutes_canmore",
|
||||
"cwc_flux_image",
|
||||
"cwc_code_interpreter",
|
||||
"cwc_code_interpreter_amsfix",
|
||||
"cwcfluxgptv",
|
||||
"flux_v3_gptv_enable_upload_multi_image_in_turn_wo_ch",
|
||||
"gptvnorm2048",
|
||||
"cwc_code_interpreter_citation_fix",
|
||||
"code_interpreter_interactive_charts",
|
||||
"cwc_code_interpreter_interactive_charts_inline_image",
|
||||
"code_interpreter_matplotlib_patching",
|
||||
"cwc_fileupload_odb",
|
||||
"update_memory_plugin",
|
||||
"add_custom_instructions",
|
||||
@@ -98,6 +133,20 @@ export const M365_DEFAULT_OPTION_SETS = [
|
||||
"flux_v3_progress_messages",
|
||||
"enable_batch_token_processing",
|
||||
"enable_gg_gpt",
|
||||
"async_client_interaction",
|
||||
"flux_v3_references",
|
||||
"flux_v3_references_entities",
|
||||
"flux_v3_references_ci",
|
||||
"add_filestore_filetype",
|
||||
"cwc_code_interpreter_citation_sourceannotations",
|
||||
"cdxcwc_code_interpreter_hallucinated_url_filter",
|
||||
"flux_v3_image_gen_enable_dimensions",
|
||||
"flux_v3_image_gen_enable_non_watermarked_storage",
|
||||
"flux_v3_image_gen_enable_icon_dimensions",
|
||||
"flux_v3_image_gen_enable_system_text_with_params",
|
||||
"flux_v3_image_gen_enable_designer_dimensions_meta_prompting_in_system_prompts",
|
||||
"flux_v3_image_gen_enable_story",
|
||||
"rich_responses",
|
||||
] as const;
|
||||
|
||||
/** Append the record separator to a JSON-serializable frame. */
|
||||
@@ -433,12 +482,14 @@ export function resolveChatInvocationOverrides(tier: string | undefined): {
|
||||
}
|
||||
return {
|
||||
optionsSets: [...M365_DEFAULT_OPTION_SETS],
|
||||
// #10718 — the 2026-08 recapture sends tone:"magic" (lowercase) on the
|
||||
// individual/EDU surface; the old "" default is part of the dropped shape.
|
||||
tone: "magic",
|
||||
// 2026-08-21 capture — the individual/consumer surface now sends "Magic"
|
||||
// (capitalized), matching the enterprise tone literal. The #10718
|
||||
// lowercase "magic" is part of the shape that gets silently dropped.
|
||||
tone: "Magic",
|
||||
allowedMessageTypes: ALLOWED_MESSAGE_TYPES,
|
||||
// Omitted entirely on the individual/EDU wire (see ChatInvocationOptions).
|
||||
disconnectBehavior: undefined,
|
||||
// 2026-08-21 capture — disconnectBehavior:"continue" is now present on the
|
||||
// individual/consumer wire too, not just enterprise (see ChatInvocationOptions).
|
||||
disconnectBehavior: "continue",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -467,16 +518,33 @@ export function resolveToneForModel(model: string | undefined): string | undefin
|
||||
|
||||
/**
|
||||
* Build the `type:4` chat invocation frame body (not yet `\x1e`-terminated).
|
||||
* Mirrors the argument shape recaptured from a working `m365.cloud.microsoft/chat`
|
||||
* client in 2026-08 (#10718). Notable differences from the pre-#10718 shape: a
|
||||
* populated `clientInfo` + `productThreadType:"Office"`, a `conversationId`
|
||||
* matching the WS URL query, a rich `message` object, and no
|
||||
* `spokenTextMode` / `extraExtensionParameters` / `isSbsSupported` /
|
||||
* `renderReferencesBehindEOS` / `disconnectBehavior` — none of those are still
|
||||
* observed on the wire, and the stale shape gets closed immediately after the
|
||||
* invocation.
|
||||
* Base shape from the #10718 recapture (populated `clientInfo` +
|
||||
* `productThreadType:"Office"`, a `conversationId` matching the WS URL query, a
|
||||
* rich `message` object), extended per a 2026-08-21 live capture that found the
|
||||
* #10718 shape alone no longer produces a `type:1 target:"update"` frame — the
|
||||
* socket only replies with keepalive pings and closes. The additions below
|
||||
* (richer `clientInfo`, non-empty `plugins`, `extraExtensionParameters`,
|
||||
* `isSbsSupported`, `renderReferencesBehindEOS`,
|
||||
* `message.connectedFederatedConnections`, and `disconnectBehavior` on every
|
||||
* tier) are exactly the fields the 2026-08-21 capture had that this shape was
|
||||
* missing; the #10718 fields (`conversationId`, `productThreadType`,
|
||||
* `toolChoice`, `message.attachments`) are kept as-is since removing them was
|
||||
* not verified against a live socket.
|
||||
*/
|
||||
export function buildChatInvocation(opts: ChatInvocationOptions): Record<string, unknown> {
|
||||
const clientInfo = {
|
||||
clientAppName: "Office",
|
||||
clientPlatform: "mcmcopilot-web",
|
||||
clientEntrypoint: "mcmcopilot-officeweb",
|
||||
clientSessionId: opts.sessionId,
|
||||
ProductCategory: "Chat",
|
||||
clientAppType: "Web",
|
||||
productEntryPoint: "ChatPanel",
|
||||
deviceOS: "Windows",
|
||||
deviceType: "Desktop",
|
||||
clientPlatformVersion: "10",
|
||||
};
|
||||
|
||||
return {
|
||||
type: 4,
|
||||
target: "chat",
|
||||
@@ -487,17 +555,17 @@ export function buildChatInvocation(opts: ChatInvocationOptions): Record<string,
|
||||
? [...opts.allowedMessageTypes]
|
||||
: [...ALLOWED_MESSAGE_TYPES],
|
||||
clientCorrelationId: opts.clientCorrelationId ?? opts.traceId,
|
||||
clientInfo: {
|
||||
clientAppName: "Office",
|
||||
clientPlatform: "mcmcopilot-web",
|
||||
},
|
||||
clientInfo,
|
||||
conversationId: opts.conversationId,
|
||||
extraExtensionParameters: {},
|
||||
isStartOfSession: opts.isStartOfSession ?? true,
|
||||
message: {
|
||||
adaptiveCards: [],
|
||||
attachments: null,
|
||||
author: "user",
|
||||
clientInfo,
|
||||
clientPreferences: {},
|
||||
connectedFederatedConnections: ["dummyId"],
|
||||
entityAnnotationTypes: ["People", "File", "Event", "Email", "TeamsMessage"],
|
||||
experienceType: "Default",
|
||||
inputMethod: "Keyboard",
|
||||
@@ -510,22 +578,27 @@ export function buildChatInvocation(opts: ChatInvocationOptions): Record<string,
|
||||
requestId: opts.requestId,
|
||||
text: opts.text,
|
||||
},
|
||||
isSbsSupported: true,
|
||||
options: {},
|
||||
optionsSets: opts.optionsSets ?? [...M365_DEFAULT_OPTION_SETS],
|
||||
plugins: opts.plugins ?? [],
|
||||
// 2026-08-21 capture (#11069): BingWebSearch is now the universal
|
||||
// BuiltIn plugin on individual/consumer tier; keep an opt-out override.
|
||||
plugins: opts.plugins ?? [{ Id: "BingWebSearch", Source: "BuiltIn" }],
|
||||
...(opts.customInstructions ? { customInstructions: opts.customInstructions } : {}),
|
||||
productThreadType: "Office",
|
||||
renderReferencesBehindEOS: true,
|
||||
sessionId: opts.sessionId,
|
||||
sliceIds: [],
|
||||
source: "officeweb",
|
||||
streamingMode: "ConciseWithPadding",
|
||||
threadLevelGptId: {},
|
||||
tone: opts.tone ?? "magic",
|
||||
// 2026-08-21 capture (#11069): tone is now capitalized "Magic" on both tiers.
|
||||
tone: opts.tone ?? "Magic",
|
||||
toolChoice: opts.toolChoice ?? null,
|
||||
traceId: opts.traceId,
|
||||
// #8971 keeps "continue" for the enterprise tier; the individual/EDU wire
|
||||
// omits the key, so only include it when actually set (#10718).
|
||||
...(opts.disconnectBehavior ? { disconnectBehavior: opts.disconnectBehavior } : {}),
|
||||
// 2026-08-21 capture — disconnectBehavior:"continue" is sent on every
|
||||
// tier now, not gated to enterprise as the #8971 comment described.
|
||||
disconnectBehavior: opts.disconnectBehavior ?? "continue",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -12,8 +12,13 @@ import {
|
||||
} from "@/lib/combos/intelligentRouting";
|
||||
import { AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
|
||||
function getI18nOrFallback(t: any, key: string, fallback: string) {
|
||||
if (typeof t?.has === "function" && t.has(key)) return t(key);
|
||||
function getI18nOrFallback(
|
||||
t: any,
|
||||
key: string,
|
||||
fallback: string,
|
||||
values?: Record<string, unknown>
|
||||
) {
|
||||
if (typeof t?.has === "function" && t.has(key)) return t(key, values);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@@ -94,10 +99,9 @@ export default function IntelligentComboPanel({
|
||||
const updatedCombo = await response.json();
|
||||
onComboUpdated?.(updatedCombo);
|
||||
notify.success(
|
||||
getI18nOrFallback(t, "modePackUpdated", "Mode pack updated to {pack}.").replace(
|
||||
"{pack}",
|
||||
modePackId
|
||||
)
|
||||
getI18nOrFallback(t, "modePackUpdated", "Mode pack updated to {pack}.", {
|
||||
pack: modePackId,
|
||||
}).replace("{pack}", modePackId)
|
||||
);
|
||||
} catch (error: any) {
|
||||
notify.error(error?.message || "Failed to update mode pack.");
|
||||
@@ -184,10 +188,9 @@ export default function IntelligentComboPanel({
|
||||
</div>
|
||||
{savingModePack && (
|
||||
<span className="text-[11px] text-text-muted">
|
||||
{getI18nOrFallback(t, "savingModePack", "Saving {pack}…").replace(
|
||||
"{pack}",
|
||||
savingModePack
|
||||
)}
|
||||
{getI18nOrFallback(t, "savingModePack", "Saving {pack}…", {
|
||||
pack: savingModePack,
|
||||
}).replace("{pack}", savingModePack)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -533,9 +533,9 @@ function getStrategyBadgeClass(strategy) {
|
||||
return "bg-blue-500/15 text-blue-600 dark:text-blue-400";
|
||||
}
|
||||
|
||||
function getI18nOrFallback(t, key, fallback) {
|
||||
function getI18nOrFallback(t, key, fallback, values) {
|
||||
try {
|
||||
if (typeof t.has === "function" && t.has(key)) return t(key);
|
||||
if (typeof t.has === "function" && t.has(key)) return t(key, values);
|
||||
} catch {}
|
||||
return fallback;
|
||||
}
|
||||
@@ -1565,7 +1565,8 @@ function StrategyRecommendationsPanel({ strategy, onApply, showNudge }) {
|
||||
{getI18nOrFallback(
|
||||
t,
|
||||
"recommendationsUpdated",
|
||||
"Recommendations updated for {strategy}."
|
||||
"Recommendations updated for {strategy}.",
|
||||
{ strategy: strategyLabel }
|
||||
).replace("{strategy}", strategyLabel)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
extractM365CredentialFromHar,
|
||||
describeHarImportExpiry,
|
||||
type M365HarImportResult,
|
||||
} from "@/shared/utils/m365HarImport";
|
||||
import { providerText, type ProviderMessageTranslator } from "../providerPageHelpers";
|
||||
|
||||
type HarImporter = (text: string) => M365HarImportResult;
|
||||
|
||||
// One entry per web-session provider that can offer HAR import. Add a new
|
||||
// key here (and its own extractor in src/shared/utils/) to support another
|
||||
// provider — the button renders nothing for any provider not listed.
|
||||
const HAR_IMPORTERS: Record<string, HarImporter> = {
|
||||
"copilot-m365-web": extractM365CredentialFromHar,
|
||||
};
|
||||
|
||||
const ERROR_MESSAGE_KEYS: Record<string, [string, string]> = {
|
||||
notJson: ["harImportErrorNotJson", "That file isn't valid JSON — is it really a .har export?"],
|
||||
noEntries: ["harImportErrorNoEntries", "This HAR has no network entries recorded."],
|
||||
noChathubUrl: [
|
||||
"harImportErrorNoChathubUrl",
|
||||
"No Copilot chat connection found in this HAR. Send at least one chat message in m365.cloud.microsoft before exporting.",
|
||||
],
|
||||
unparsableUrl: [
|
||||
"harImportErrorUnparsableUrl",
|
||||
"Found the chat connection, but couldn't read its URL.",
|
||||
],
|
||||
missingFields: [
|
||||
"harImportErrorMissingFields",
|
||||
"Found the chat connection, but the token was missing from it.",
|
||||
],
|
||||
};
|
||||
|
||||
export interface HarImportButtonProps {
|
||||
provider: string;
|
||||
onImport: (apiKey: string) => void;
|
||||
}
|
||||
|
||||
export default function HarImportButton({ provider, onImport }: HarImportButtonProps) {
|
||||
const t = useTranslations("providers") as ProviderMessageTranslator;
|
||||
const importer = HAR_IMPORTERS[provider];
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [state, setState] = useState<
|
||||
| { phase: "idle" }
|
||||
| { phase: "reading" }
|
||||
| { phase: "error"; message: string }
|
||||
| { phase: "success"; expiresAt: number | null }
|
||||
>({ phase: "idle" });
|
||||
|
||||
if (!importer) return null;
|
||||
|
||||
async function handleFile(file: File | undefined) {
|
||||
if (!file) return;
|
||||
setState({ phase: "reading" });
|
||||
let text: string;
|
||||
try {
|
||||
text = await file.text();
|
||||
} catch {
|
||||
setState({
|
||||
phase: "error",
|
||||
message: providerText(t, "harImportErrorReadFailed", "Couldn't read that file."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = importer(text);
|
||||
if (!result.ok) {
|
||||
const [key, fallback] = ERROR_MESSAGE_KEYS[result.error] ?? [
|
||||
"harImportErrorUnknown",
|
||||
"Couldn't extract a credential from that HAR file.",
|
||||
];
|
||||
setState({ phase: "error", message: providerText(t, key, fallback) });
|
||||
return;
|
||||
}
|
||||
|
||||
onImport(result.apiKey);
|
||||
setState({ phase: "success", expiresAt: result.expiresAt });
|
||||
}
|
||||
|
||||
const expiry = state.phase === "success" ? describeHarImportExpiry(state.expiresAt) : null;
|
||||
const expiryText =
|
||||
expiry?.tone === "unknown"
|
||||
? providerText(t, "harImportStatusUnknownExpiry", "Imported. Couldn't read its expiry.")
|
||||
: expiry?.tone === "bad"
|
||||
? providerText(
|
||||
t,
|
||||
"harImportStatusExpired",
|
||||
"Imported, but this token already expired ({minutes}m ago) — export a fresh HAR.",
|
||||
{ minutes: Math.abs(expiry.minutesRemaining ?? 0) }
|
||||
)
|
||||
: expiry?.tone === "warn"
|
||||
? providerText(
|
||||
t,
|
||||
"harImportStatusExpiringSoon",
|
||||
"Imported — valid for only ~{minutes}m more.",
|
||||
{ minutes: expiry.minutesRemaining ?? 0 }
|
||||
)
|
||||
: expiry?.tone === "ok"
|
||||
? providerText(t, "harImportStatusValid", "Imported — valid for ~{minutes}m.", {
|
||||
minutes: expiry.minutesRemaining ?? 0,
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={state.phase === "reading"}
|
||||
data-testid="har-import-button"
|
||||
className="inline-flex items-center gap-1.5 rounded border border-border px-2.5 py-1.5 text-xs font-medium text-text-main hover:bg-surface-hover disabled:opacity-50"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
|
||||
upload_file
|
||||
</span>
|
||||
{state.phase === "reading"
|
||||
? providerText(t, "harImportButtonBusy", "Importing…")
|
||||
: providerText(t, "harImportButtonLabel", "Import .har file")}
|
||||
</button>
|
||||
<span className="text-xs text-text-muted">
|
||||
{providerText(
|
||||
t,
|
||||
"harImportButtonHint",
|
||||
"Export from DevTools Network tab after sending at least one chat message."
|
||||
)}
|
||||
</span>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".har,application/json"
|
||||
data-testid="har-import-input"
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
void handleFile(event.target.files?.[0]);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{state.phase === "error" && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400" data-testid="har-import-error">
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
{state.phase === "success" && expiryText && (
|
||||
<p
|
||||
className={
|
||||
expiry?.tone === "bad"
|
||||
? "text-xs text-red-600 dark:text-red-400"
|
||||
: expiry?.tone === "warn"
|
||||
? "text-xs text-amber-700 dark:text-amber-300"
|
||||
: "text-xs text-emerald-700 dark:text-emerald-300"
|
||||
}
|
||||
data-testid="har-import-status"
|
||||
>
|
||||
{expiryText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { getWebSessionCredentialRequirement } from "../../webSessionCredentials";
|
||||
import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
|
||||
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
|
||||
import HarImportButton from "../HarImportButton";
|
||||
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
|
||||
import { buildAddProviderSpecificData } from "./connectionProviderSpecificData";
|
||||
import { getCommandCodeAuthPhaseLabel } from "./commandCodeAuthPhase";
|
||||
@@ -209,13 +210,13 @@ export default function AddApiKeyModal({
|
||||
? "Freebuff uses an authentic CLI auth token obtained via codebuff CLI login or automated harvester."
|
||||
: isWebSessionCredential
|
||||
? getWebSessionCredentialHint(t, webSessionCredential, providerDisplayName, false)
|
||||
: isLocalSelfHostedProvider
|
||||
? t("localProviderApiKeyOptionalHint", {
|
||||
provider: localProviderMetadata?.name || providerName || provider || "",
|
||||
})
|
||||
: apiKeyOptional
|
||||
? t("apiKeyOptionalHint")
|
||||
: undefined;
|
||||
: isLocalSelfHostedProvider
|
||||
? t("localProviderApiKeyOptionalHint", {
|
||||
provider: localProviderMetadata?.name || providerName || provider || "",
|
||||
})
|
||||
: apiKeyOptional
|
||||
? t("apiKeyOptionalHint")
|
||||
: undefined;
|
||||
const credentialValidationFailedMessage = isWebSessionCredential
|
||||
? providerText(
|
||||
t,
|
||||
@@ -750,6 +751,12 @@ export default function AddApiKeyModal({
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
{provider && (
|
||||
<HarImportButton
|
||||
provider={provider}
|
||||
onImport={(apiKey) => setFormData({ ...formData, apiKey })}
|
||||
/>
|
||||
)}
|
||||
{!isNoAuthWebSessionCredential && (
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
import { getWebSessionCredentialRequirement } from "../../webSessionCredentials";
|
||||
import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
|
||||
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
|
||||
import HarImportButton from "../HarImportButton";
|
||||
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
|
||||
import { CodexConnectionFields } from "./CodexFingerprintFields";
|
||||
import { assignEditApiKeyProviderSpecificData } from "./connectionProviderSpecificData";
|
||||
@@ -909,6 +910,12 @@ export default function EditConnectionModal({
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
{provider && (
|
||||
<HarImportButton
|
||||
provider={provider}
|
||||
onImport={(apiKey) => setFormData({ ...formData, apiKey })}
|
||||
/>
|
||||
)}
|
||||
{!isNoAuthWebSessionCredential && (
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
|
||||
@@ -6269,6 +6269,20 @@
|
||||
"webSessionGuideStep3": "Copy the required credential from the provider's own domain. For cookies, copy only the Cookie header value and omit Cookie:.",
|
||||
"webSessionGuideStep3Manual": "Manual path: open the browser developer tools (F12 → Network), refresh the page, open an authenticated request, and copy the Cookie header value from Request Headers — omit the Cookie: prefix.",
|
||||
"webSessionGuideStep4": "Paste it here and check the connection. If it stops working, sign in again and replace it with a fresh value.",
|
||||
"harImportButtonLabel": "Import .har file",
|
||||
"harImportButtonBusy": "Importing…",
|
||||
"harImportButtonHint": "Export from DevTools Network tab after sending at least one chat message.",
|
||||
"harImportStatusValid": "Imported — valid for ~{minutes}m.",
|
||||
"harImportStatusExpiringSoon": "Imported — valid for only ~{minutes}m more.",
|
||||
"harImportStatusExpired": "Imported, but this token already expired ({minutes}m ago) — export a fresh HAR.",
|
||||
"harImportStatusUnknownExpiry": "Imported. Couldn't read its expiry.",
|
||||
"harImportErrorNotJson": "That file isn't valid JSON — is it really a .har export?",
|
||||
"harImportErrorNoEntries": "This HAR has no network entries recorded.",
|
||||
"harImportErrorNoChathubUrl": "No Copilot chat connection found in this HAR. Send at least one chat message in m365.cloud.microsoft before exporting.",
|
||||
"harImportErrorUnparsableUrl": "Found the chat connection, but couldn't read its URL.",
|
||||
"harImportErrorMissingFields": "Found the chat connection, but the token was missing from it.",
|
||||
"harImportErrorReadFailed": "Couldn't read that file.",
|
||||
"harImportErrorUnknown": "Couldn't extract a credential from that HAR file.",
|
||||
"webSessionSecurityHint": "Treat this like a password: it may access your signed-in web account until it expires or is revoked.",
|
||||
"webNoAuthGuideTitle": "No credential required",
|
||||
"webNoAuthGuideBody": "{provider} does not need an API key or cookie. Save the connection to use its free web endpoint.",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// OpenAI/Gemini-format + Bedrock provider key validators (bedrock, openai-like, command-code, gemini-like, openai-compatible).
|
||||
// Extracted from validation.ts (god-file decomposition) — top-level functions; behavior is
|
||||
// byte-identical to the original inline defs.
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||
import {
|
||||
discoverBedrockNativeModels,
|
||||
@@ -196,13 +195,12 @@ export async function validateOpenAILikeProvider({
|
||||
export async function validateCommandCodeProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
const entry = getRegistryEntry("command-code");
|
||||
const baseUrl = normalizeBaseUrl(entry?.baseUrl || "https://api.commandcode.ai");
|
||||
const chatPath = entry?.chatPath || "/alpha/generate";
|
||||
const chatPath = entry?.chatPath || "/provider/v1/chat/completions";
|
||||
const url = `${baseUrl}${chatPath.startsWith("/") ? chatPath : `/${chatPath}`}`;
|
||||
const validationModelId =
|
||||
providerSpecificData?.validationModelId ||
|
||||
entry?.models?.find((model) => model.id === "deepseek/deepseek-v4-flash")?.id ||
|
||||
"deepseek/deepseek-v4-flash";
|
||||
const { COMMAND_CODE_VERSION } = await import("@omniroute/open-sse/executors/commandCode.ts");
|
||||
|
||||
return validateDirectChatProvider({
|
||||
url,
|
||||
@@ -210,37 +208,13 @@ export async function validateCommandCodeProvider({ apiKey, providerSpecificData
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"x-command-code-version": COMMAND_CODE_VERSION,
|
||||
"x-cli-environment": "external",
|
||||
"x-project-slug": "pi-cc",
|
||||
"x-taste-learning": "false",
|
||||
"x-co-flag": "false",
|
||||
"x-session-id": randomUUID(),
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: {
|
||||
config: {
|
||||
workingDir: "/workspace",
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
environment: "external",
|
||||
structure: [],
|
||||
isGitRepo: false,
|
||||
currentBranch: "",
|
||||
mainBranch: "",
|
||||
gitStatus: "",
|
||||
recentCommits: [],
|
||||
},
|
||||
memory: "",
|
||||
taste: "",
|
||||
skills: "",
|
||||
permissionMode: "standard",
|
||||
params: {
|
||||
model: validationModelId,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
tools: [],
|
||||
system: "",
|
||||
max_tokens: 1,
|
||||
stream: true,
|
||||
},
|
||||
model: validationModelId,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
stream: true,
|
||||
max_tokens: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
|
||||
textIcon: "CC",
|
||||
website: "https://commandcode.ai/",
|
||||
authHint:
|
||||
"Use a Command Code API key. Requests are sent to Command Code's /alpha/generate endpoint.",
|
||||
"Use a Command Code API key. Requests are sent to Command Code's /provider/v1/chat/completions endpoint.",
|
||||
apiHint: "Create or copy an API key from Command Code, then paste it here as a Bearer token.",
|
||||
},
|
||||
openrouter: {
|
||||
|
||||
116
src/shared/utils/m365HarImport.ts
Normal file
116
src/shared/utils/m365HarImport.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Pure, transport-free helpers that extract a `copilot-m365-web` credential
|
||||
* (`access_token=...; chathubPath=...`) directly from a DevTools HAR export,
|
||||
* so the "Add connection" / "Edit connection" dialogs can offer a one-click
|
||||
* "Import .har file" button instead of the user hand-extracting the token
|
||||
* from the WebSocket URL (see `HarImportButton.tsx`).
|
||||
*
|
||||
* The token rides in the query string of the BizChat ChatHub WebSocket URL
|
||||
* (`wss://substrate.office.com/m365Copilot/Chathub/<oid>@<tenant>?...
|
||||
* access_token=...`) — the same URL the executor itself connects to (see
|
||||
* `open-sse/executors/copilot-m365-connection.ts`). No network calls here;
|
||||
* everything operates on the HAR text already in the browser.
|
||||
*/
|
||||
|
||||
/** Prefix identifying the BizChat ChatHub WebSocket request in a HAR entry. */
|
||||
export const M365_CHATHUB_WS_PREFIX = "wss://substrate.office.com/m365Copilot/Chathub/";
|
||||
|
||||
export type M365HarImportResult =
|
||||
| { ok: true; apiKey: string; chathubPath: string; expiresAt: number | null }
|
||||
| { ok: false; error: string };
|
||||
|
||||
interface HarEntryLike {
|
||||
request?: { url?: unknown };
|
||||
}
|
||||
|
||||
interface HarLike {
|
||||
log?: { entries?: unknown };
|
||||
}
|
||||
|
||||
/** Decode a JWT payload WITHOUT verification — exp is a display hint only. */
|
||||
function decodeJwtExpiry(token: string): number | null {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return null;
|
||||
try {
|
||||
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
|
||||
const json = JSON.parse(atob(padded)) as { exp?: unknown };
|
||||
return typeof json.exp === "number" ? json.exp * 1000 : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the `copilot-m365-web` credential from raw HAR file text.
|
||||
*
|
||||
* Scans every request in `log.entries` for the ChatHub WebSocket URL and
|
||||
* uses the LAST match (a HAR may contain several turns of the same session;
|
||||
* the most recent one carries the freshest token). Returns a structured
|
||||
* error — never throws — so callers can render it directly.
|
||||
*/
|
||||
export function extractM365CredentialFromHar(text: string): M365HarImportResult {
|
||||
let har: HarLike;
|
||||
try {
|
||||
har = JSON.parse(text) as HarLike;
|
||||
} catch {
|
||||
return { ok: false, error: "notJson" };
|
||||
}
|
||||
|
||||
const entries = har.log?.entries;
|
||||
if (!Array.isArray(entries)) {
|
||||
return { ok: false, error: "noEntries" };
|
||||
}
|
||||
|
||||
let matchedUrl: string | null = null;
|
||||
for (const entry of entries as HarEntryLike[]) {
|
||||
const url = entry?.request?.url;
|
||||
if (typeof url === "string" && url.startsWith(M365_CHATHUB_WS_PREFIX)) {
|
||||
matchedUrl = url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedUrl) {
|
||||
return { ok: false, error: "noChathubUrl" };
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(matchedUrl);
|
||||
} catch {
|
||||
return { ok: false, error: "unparsableUrl" };
|
||||
}
|
||||
|
||||
const token = parsed.searchParams.get("access_token");
|
||||
const chathubPath = decodeURIComponent(parsed.pathname.split("/m365Copilot/Chathub/")[1] || "");
|
||||
|
||||
if (!token || !chathubPath) {
|
||||
return { ok: false, error: "missingFields" };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
apiKey: `access_token=${token}; chathubPath=${chathubPath}`,
|
||||
chathubPath,
|
||||
expiresAt: decodeJwtExpiry(token),
|
||||
};
|
||||
}
|
||||
|
||||
export type HarImportExpiryTone = "ok" | "warn" | "bad" | "unknown";
|
||||
|
||||
export interface HarImportExpiryStatus {
|
||||
tone: HarImportExpiryTone;
|
||||
minutesRemaining: number | null;
|
||||
}
|
||||
|
||||
/** Classify a decoded token expiry for the inline status hint. */
|
||||
export function describeHarImportExpiry(
|
||||
expiresAt: number | null,
|
||||
now: number = Date.now()
|
||||
): HarImportExpiryStatus {
|
||||
if (expiresAt === null) return { tone: "unknown", minutesRemaining: null };
|
||||
const minutesRemaining = Math.round((expiresAt - now) / 60000);
|
||||
if (minutesRemaining <= 0) return { tone: "bad", minutesRemaining };
|
||||
if (minutesRemaining < 15) return { tone: "warn", minutesRemaining };
|
||||
return { tone: "ok", minutesRemaining };
|
||||
}
|
||||
@@ -8,20 +8,13 @@ const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-command-c
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const { REGISTRY, getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { CommandCodeExecutor, COMMAND_CODE_VERSION } =
|
||||
await import("../../open-sse/executors/commandCode.ts");
|
||||
const { CommandCodeExecutor } = await import("../../open-sse/executors/commandCode.ts");
|
||||
const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts");
|
||||
const { createResponsesApiTransformStream } =
|
||||
await import("../../open-sse/transformer/responsesTransformer.ts");
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type ResponsesEvent = {
|
||||
event: string;
|
||||
data: { response: JsonRecord & { usage?: unknown; output?: JsonRecord[] } };
|
||||
};
|
||||
type FetchCall = { url: string; init: Record<string, unknown>; body?: Record<string, unknown> };
|
||||
|
||||
const PINNED_COMMAND_CODE_MODELS = [
|
||||
"claude-opus-4-7",
|
||||
@@ -44,20 +37,7 @@ const PINNED_COMMAND_CODE_MODELS = [
|
||||
"Qwen/Qwen3.6-Plus",
|
||||
];
|
||||
|
||||
function commandCodeStream(lines: unknown[], { sse = false } = {}) {
|
||||
const text = lines
|
||||
.map((line) => {
|
||||
const json = JSON.stringify(line);
|
||||
return sse ? `data: ${json}\n\n` : `${json}\n`;
|
||||
})
|
||||
.join("");
|
||||
return new Response(text, { status: 200, headers: { "Content-Type": "application/x-ndjson" } });
|
||||
}
|
||||
|
||||
function toPlainHeaders(headers: Headers | Record<string, string>) {
|
||||
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
|
||||
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)]));
|
||||
}
|
||||
const CHAT_URL = "https://api.commandcode.ai/provider/v1/chat/completions";
|
||||
|
||||
function parseSsePayloads(sse: string) {
|
||||
return sse
|
||||
@@ -68,25 +48,21 @@ function parseSsePayloads(sse: string) {
|
||||
.map((line) => JSON.parse(line));
|
||||
}
|
||||
|
||||
async function responsesFromChatSse(sse: string): Promise<ResponsesEvent[]> {
|
||||
const input = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(sse));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const transformed = await new Response(
|
||||
input.pipeThrough(createResponsesApiTransformStream(null, 60_000))
|
||||
).text();
|
||||
function openAiSse(obj: unknown): string {
|
||||
return `data: ${JSON.stringify(obj)}\n\n`;
|
||||
}
|
||||
|
||||
return transformed
|
||||
.split("\n\n")
|
||||
.map((part) => {
|
||||
const event = part.match(/^event:\s*(.+)$/m)?.[1];
|
||||
const data = part.match(/^data:\s*(.+)$/m)?.[1];
|
||||
return event && data ? ({ event, data: JSON.parse(data) } as ResponsesEvent) : null;
|
||||
})
|
||||
.filter((entry): entry is ResponsesEvent => entry !== null);
|
||||
function captureFetch(body: Record<string, unknown>) {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({
|
||||
url: String(url),
|
||||
init,
|
||||
body: JSON.parse(String(init.body)),
|
||||
});
|
||||
return new Response(JSON.stringify(body), { status: 200 });
|
||||
};
|
||||
return calls;
|
||||
}
|
||||
|
||||
test.afterEach(() => {
|
||||
@@ -105,7 +81,9 @@ test("Command Code provider catalog has pinned models and alias lookup", () => {
|
||||
assert.equal(entry.alias, "cmd");
|
||||
assert.equal(entry.executor, "command-code");
|
||||
assert.equal(entry.baseUrl, "https://api.commandcode.ai");
|
||||
assert.equal(entry.chatPath, "/alpha/generate");
|
||||
// Chat targets the documented /provider/v1/chat/completions endpoint, NOT the
|
||||
// CLI-only /alpha/generate endpoint (#10265).
|
||||
assert.equal(entry.chatPath, "/provider/v1/chat/completions");
|
||||
assert.deepEqual(
|
||||
entry.models.map((model) => model.id),
|
||||
PINNED_COMMAND_CODE_MODELS
|
||||
@@ -119,17 +97,10 @@ test("getExecutor returns the specialized Command Code executor", () => {
|
||||
assert.ok(getExecutor("cmd") instanceof CommandCodeExecutor);
|
||||
});
|
||||
|
||||
type FetchCall = { url: string; init: Record<string, unknown>; body?: unknown };
|
||||
|
||||
test("Command Code executor posts wrapped body and required headers to /alpha/generate", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return commandCodeStream([{ type: "text-delta", text: "hello" }, { type: "finish" }]);
|
||||
};
|
||||
|
||||
test("Command Code executor posts a flat OpenAI body + standard headers to /provider/v1/chat/completions (#10265)", async () => {
|
||||
const calls = captureFetch({});
|
||||
const executor = getExecutor("command-code");
|
||||
const { response, url, headers, transformedBody } = await executor.execute({
|
||||
const { response, url, headers } = await executor.execute({
|
||||
model: "gpt-5.4-mini",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
@@ -144,41 +115,34 @@ test("Command Code executor posts wrapped body and required headers to /alpha/ge
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(url, "https://api.commandcode.ai/alpha/generate");
|
||||
assert.equal(url, CHAT_URL);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].url, "https://api.commandcode.ai/alpha/generate");
|
||||
assert.equal(calls[0].url, CHAT_URL);
|
||||
assert.equal(calls[0].init.method, "POST");
|
||||
assert.equal(headers.Authorization, "Bearer cc_test_key");
|
||||
assert.equal(headers["x-command-code-version"], COMMAND_CODE_VERSION);
|
||||
assert.equal(headers["x-cli-environment"], "external");
|
||||
assert.equal(headers["x-project-slug"], "pi-cc");
|
||||
assert.equal(headers["x-taste-learning"], "false");
|
||||
assert.equal(headers["x-co-flag"], "false");
|
||||
assert.equal(typeof headers["x-session-id"], "string");
|
||||
// No CLI-impersonation headers.
|
||||
assert.equal(headers["x-command-code-version"], undefined);
|
||||
assert.equal(headers["x-cli-environment"], undefined);
|
||||
assert.equal(headers["x-project-slug"], undefined);
|
||||
|
||||
const posted = JSON.parse(String(calls[0].init.body));
|
||||
assert.deepEqual(posted, transformedBody);
|
||||
for (const key of ["config", "memory", "taste", "skills", "permissionMode", "params"]) {
|
||||
assert.ok(key in posted, `missing ${key}`);
|
||||
}
|
||||
assert.equal(posted.skills, "");
|
||||
assert.equal(posted.params.model, "gpt-5.4-mini");
|
||||
assert.equal(posted.params.stream, true);
|
||||
assert.equal(posted.params.system, "You are concise.");
|
||||
assert.equal(posted.params.messages[0].role, "user");
|
||||
assert.equal(posted.params.tools[0].name, "lookup");
|
||||
const posted = calls[0].body as Record<string, unknown>;
|
||||
// No CLI envelope.
|
||||
assert.equal(posted.config, undefined, "CLI envelope config must not be sent");
|
||||
assert.equal(posted.params, undefined, "CLI envelope params wrapper must not be sent");
|
||||
assert.equal(posted.model, "gpt-5.4-mini");
|
||||
assert.equal(posted.stream, false);
|
||||
assert.equal((posted.messages as Array<{ role: string }>)[0].role, "system");
|
||||
const tool = (posted.tools as Array<{ function: { name: string } }>)[0];
|
||||
assert.equal(tool.function.name, "lookup", "tools in OpenAI shape (function.name)");
|
||||
assert.equal(posted.max_tokens, 42);
|
||||
|
||||
// The upstream OpenAI JSON passes through untouched.
|
||||
const json = await response.json();
|
||||
assert.equal(json.choices[0].message.content, "hello");
|
||||
assert.deepEqual(json, {});
|
||||
});
|
||||
|
||||
test("Command Code executor passes reasoning/thinking fields through to params (#2986 follow-up)", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]);
|
||||
};
|
||||
|
||||
test("Command Code executor passes reasoning/thinking fields through at the top level of the OpenAI body", async () => {
|
||||
const calls = captureFetch({});
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
@@ -189,30 +153,19 @@ test("Command Code executor passes reasoning/thinking fields through to params (
|
||||
reasoning_effort: "high",
|
||||
thinking: { type: "enabled" },
|
||||
effort: "high",
|
||||
output_config: { effort: "high" },
|
||||
extra_body: { enable_thinking: true },
|
||||
},
|
||||
});
|
||||
|
||||
const posted = JSON.parse(String(calls[0].init.body));
|
||||
assert.equal(posted.params.reasoning_effort, "high");
|
||||
assert.deepEqual(posted.params.thinking, { type: "enabled" });
|
||||
assert.equal(posted.params.effort, "high");
|
||||
assert.deepEqual(posted.params.output_config, { effort: "high" });
|
||||
assert.deepEqual(posted.params.extra_body, { enable_thinking: true });
|
||||
const posted = calls[0].body as Record<string, unknown>;
|
||||
assert.equal(posted.reasoning_effort, "high");
|
||||
assert.deepEqual(posted.thinking, { type: "enabled" });
|
||||
assert.equal(posted.effort, "high");
|
||||
assert.deepEqual(posted.extra_body, { enable_thinking: true });
|
||||
});
|
||||
|
||||
test("Command Code executor honors body.model rewrite from payload rules", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]);
|
||||
};
|
||||
|
||||
// Simulate a payload-rule rewrite: combo resolves to "deepseek-v4-pro-max"
|
||||
// (passed as the execute() model arg), but the payload rule overwrites
|
||||
// body.model to "deepseek/deepseek-v4-pro" (the vendor-prefixed form
|
||||
// Command Code's API expects).
|
||||
const calls = captureFetch({});
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek-v4-pro-max",
|
||||
stream: false,
|
||||
@@ -225,20 +178,13 @@ test("Command Code executor honors body.model rewrite from payload rules", async
|
||||
},
|
||||
});
|
||||
|
||||
const posted = JSON.parse(String(calls[0].init.body));
|
||||
assert.equal(posted.params.model, "deepseek/deepseek-v4-pro");
|
||||
assert.equal(posted.params.reasoning_effort, "max");
|
||||
const posted = calls[0].body as Record<string, unknown>;
|
||||
assert.equal(posted.model, "deepseek/deepseek-v4-pro");
|
||||
assert.equal(posted.reasoning_effort, "max");
|
||||
});
|
||||
|
||||
test("Command Code executor maps unsupported minimal reasoning_effort to low (upstream 400 regression)", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]);
|
||||
};
|
||||
|
||||
// Live upstream rejection: "Validation error: Invalid option: expected one of
|
||||
// \"low\"|\"medium\"|\"high\"|\"xhigh\"|\"max\" at \"params.reasoning_effort\"" —
|
||||
const calls = captureFetch({});
|
||||
// `minimal` (a Muse Spark catalog tier) must be downgraded to `low` before
|
||||
// the wire body is built, on BOTH the combo and single-model paths.
|
||||
await getExecutor("command-code").execute({
|
||||
@@ -252,20 +198,38 @@ test("Command Code executor maps unsupported minimal reasoning_effort to low (up
|
||||
},
|
||||
});
|
||||
|
||||
const posted = JSON.parse(String(calls[0].init.body));
|
||||
assert.equal(posted.params.reasoning_effort, "low", "minimal must map to low");
|
||||
const posted = calls[0].body as Record<string, unknown>;
|
||||
assert.equal(posted.reasoning_effort, "low", "minimal must map to low");
|
||||
});
|
||||
|
||||
test("Command Code raw NDJSON stream becomes OpenAI chat SSE chunks", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
test("Command Code executor passes the upstream OpenAI SSE stream through untouched", async () => {
|
||||
const sse =
|
||||
openAiSse({
|
||||
id: "c1",
|
||||
object: "chat.completion.chunk",
|
||||
model: "gpt-5.4",
|
||||
choices: [{ index: 0, delta: { role: "assistant" } }],
|
||||
}) +
|
||||
openAiSse({
|
||||
id: "c1",
|
||||
object: "chat.completion.chunk",
|
||||
model: "gpt-5.4",
|
||||
choices: [{ index: 0, delta: { content: "Hello" } }],
|
||||
}) +
|
||||
openAiSse({
|
||||
id: "c1",
|
||||
object: "chat.completion.chunk",
|
||||
model: "gpt-5.4",
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
}) +
|
||||
"data: [DONE]\n\n";
|
||||
let capturedStreamFlag: unknown = null;
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) });
|
||||
return commandCodeStream([
|
||||
{ type: "text-delta", text: "Hello" },
|
||||
{ type: "reasoning-delta", text: "thinking" },
|
||||
{ type: "tool-call", toolCallId: "call_1", toolName: "search", input: { q: "docs" } },
|
||||
{ type: "finish", finishReason: "tool-calls" },
|
||||
]);
|
||||
capturedStreamFlag = JSON.parse(String(init.body)).stream;
|
||||
return new Response(sse, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
};
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
@@ -275,39 +239,32 @@ test("Command Code raw NDJSON stream becomes OpenAI chat SSE chunks", async () =
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
|
||||
assert.equal(calls[0].body.params.stream, true);
|
||||
assert.equal(response.headers.get("Content-Type"), "text/event-stream; charset=utf-8");
|
||||
const sse = await response.text();
|
||||
assert.match(sse, /data: \[DONE\]/);
|
||||
const chunks = parseSsePayloads(sse);
|
||||
assert.equal(chunks[0].object, "chat.completion.chunk");
|
||||
assert.deepEqual(chunks[0].choices[0].delta, { role: "assistant" });
|
||||
assert.equal(capturedStreamFlag, true, "stream flag forwarded to upstream");
|
||||
const text = await response.text();
|
||||
assert.equal(text, sse, "OpenAI SSE stream passed through byte-for-byte");
|
||||
assert.ok(text.includes("data: [DONE]"));
|
||||
const chunks = parseSsePayloads(text);
|
||||
assert.equal(chunks[0].choices[0].delta.role, "assistant");
|
||||
assert.equal(chunks[1].choices[0].delta.content, "Hello");
|
||||
assert.equal(chunks[2].choices[0].delta.reasoning_content, "thinking");
|
||||
assert.equal(chunks[3].choices[0].delta.tool_calls[0].function.name, "search");
|
||||
assert.equal(chunks.at(-1).choices[0].finish_reason, "tool_calls");
|
||||
assert.equal(chunks[2].choices[0].finish_reason, "stop");
|
||||
});
|
||||
|
||||
test("Command Code data: SSE lines aggregate into non-stream ChatCompletion JSON", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream(
|
||||
[
|
||||
{ type: "text-delta", text: "Hel" },
|
||||
{ type: "text-delta", text: "lo" },
|
||||
{ type: "reasoning-delta", text: "because" },
|
||||
{ type: "tool-call", id: "call_2", name: "lookup", arguments: { id: 7 } },
|
||||
{
|
||||
type: "finish",
|
||||
finishReason: "max_tokens",
|
||||
totalUsage: {
|
||||
inputTokens: 3,
|
||||
inputTokenDetails: { cacheReadTokens: 2 },
|
||||
outputTokens: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
{ sse: true }
|
||||
);
|
||||
test("Command Code executor passes the upstream OpenAI JSON through untouched (non-stream)", async () => {
|
||||
const upstreamJson = {
|
||||
id: "chatcmpl-1",
|
||||
object: "chat.completion",
|
||||
model: "gpt-5.4-mini",
|
||||
choices: [{ index: 0, message: { role: "assistant", content: "Hello" }, finish_reason: "stop" }],
|
||||
usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 },
|
||||
};
|
||||
let capturedStreamFlag: unknown = null;
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
capturedStreamFlag = JSON.parse(String(init.body)).stream;
|
||||
return new Response(JSON.stringify(upstreamJson), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
@@ -316,88 +273,12 @@ test("Command Code data: SSE lines aggregate into non-stream ChatCompletion JSON
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
|
||||
assert.equal(response.headers.get("Content-Type"), "application/json");
|
||||
const json = await response.json();
|
||||
assert.equal(json.object, "chat.completion");
|
||||
assert.equal(json.choices[0].message.content, "Hello");
|
||||
assert.equal(json.choices[0].message.reasoning_content, "because");
|
||||
assert.equal(json.choices[0].message.tool_calls[0].function.arguments, JSON.stringify({ id: 7 }));
|
||||
assert.equal(json.choices[0].finish_reason, "length");
|
||||
assert.deepEqual(json.usage, {
|
||||
prompt_tokens: 3,
|
||||
prompt_tokens_details: { cached_tokens: 2 },
|
||||
completion_tokens: 5,
|
||||
completion_tokens_details: { reasoning_tokens: 0 },
|
||||
total_tokens: 8,
|
||||
cache_read_input_tokens: 2,
|
||||
});
|
||||
assert.equal(capturedStreamFlag, false, "stream flag forwarded as false for non-stream");
|
||||
assert.deepEqual(await response.json(), upstreamJson);
|
||||
});
|
||||
|
||||
test("Command Code reasoning-only output falls back to reasoning as content (non-stream)", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream(
|
||||
[
|
||||
{ type: "reasoning-delta", text: "The user wants 79874+93658. " },
|
||||
{ type: "reasoning-delta", text: "That equals 173532." },
|
||||
{
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
totalUsage: { inputTokens: 20, outputTokens: 64, outputTokenDetails: { reasoningTokens: 61 } },
|
||||
},
|
||||
],
|
||||
{ sse: true }
|
||||
);
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "meta/muse-spark-1.2-contributor",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Calculate 79874+93658, and reply with the result only." }] },
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
const message = json.choices[0].message;
|
||||
// Regression #10986: when the model emits only reasoning-delta events (never a
|
||||
// text-delta), content must fall back to the reasoning text instead of "" (which
|
||||
// OpenAI-compatible clients treat as null/no answer).
|
||||
assert.equal(message.content, "The user wants 79874+93658. That equals 173532.");
|
||||
// reasoning_content must STAY populated for reasoning-aware clients.
|
||||
assert.equal(message.reasoning_content, "The user wants 79874+93658. That equals 173532.");
|
||||
});
|
||||
|
||||
test("Command Code reasoning-only output emits a content delta chunk when streaming", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream(
|
||||
[
|
||||
{ type: "reasoning-delta", text: "The result is 173532." },
|
||||
{ type: "finish", finishReason: "stop" },
|
||||
],
|
||||
{ sse: true }
|
||||
);
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "meta/muse-spark-1.2-contributor",
|
||||
stream: true,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Calcular 79874+93658" }] },
|
||||
});
|
||||
|
||||
const sse = await response.text();
|
||||
assert.match(sse, /data: \[DONE\]/);
|
||||
const chunks = parseSsePayloads(sse);
|
||||
assert.equal(chunks[0].choices[0].delta.role, "assistant");
|
||||
// Regression #10986: the reasoning-only stream must emit a content delta when it
|
||||
// otherwise ends with no content. reasoning_content stays present too.
|
||||
const contentDelta = chunks.find((c) => c.choices[0].delta.content !== undefined);
|
||||
assert.equal(contentDelta.choices[0].delta.content, "The result is 173532.");
|
||||
const reasoningDelta = chunks.find((c) => c.choices[0].delta.reasoning_content !== undefined);
|
||||
assert.equal(reasoningDelta.choices[0].delta.reasoning_content, "The result is 173532.");
|
||||
assert.equal(chunks.at(-1).choices[0].finish_reason, "stop");
|
||||
});
|
||||
|
||||
test("Command Code executor surfaces upstream and streamed errors", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
new Response("bad key", { status: 401, statusText: "Unauthorized" });
|
||||
test("Command Code executor surfaces upstream errors", async () => {
|
||||
globalThis.fetch = async () => new Response("bad key", { status: 401, statusText: "Unauthorized" });
|
||||
const upstreamFailure = await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
stream: false,
|
||||
@@ -406,124 +287,68 @@ test("Command Code executor surfaces upstream and streamed errors", async () =>
|
||||
});
|
||||
assert.equal(upstreamFailure.response.status, 401);
|
||||
assert.equal(await upstreamFailure.response.text(), "bad key");
|
||||
|
||||
globalThis.fetch = async () => commandCodeStream([{ type: "error", error: { message: "boom" } }]);
|
||||
await assert.rejects(async () => {
|
||||
await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
}, /boom/);
|
||||
});
|
||||
|
||||
test("Command Code executor omits max_tokens when the client does not supply one (GLM-5.x)", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) });
|
||||
return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]);
|
||||
};
|
||||
|
||||
// No client max_tokens: we must NOT fabricate one. Omitting the field lets
|
||||
// Command Code's upstream apply the model's own native default.
|
||||
test("Command Code executor omits max_tokens when the client does not supply one", async () => {
|
||||
const calls = captureFetch({});
|
||||
await getExecutor("command-code").execute({
|
||||
model: "zai-org/GLM-5.1",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
assert.ok(!("max_tokens" in calls[0].body.params));
|
||||
});
|
||||
|
||||
test("Command Code executor omits max_tokens for DeepSeek v4 when the client does not supply one", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) });
|
||||
return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]);
|
||||
};
|
||||
|
||||
// Regression: previously the executor invented max_tokens from the registry
|
||||
// (384000), which /alpha/generate rejects with a 400
|
||||
// "Too big: expected number to be <=200000". With no client value we now omit
|
||||
// the field entirely, so the request succeeds and upstream picks the default.
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
assert.ok(!("max_tokens" in calls[0].body.params));
|
||||
const posted = calls[0].body as Record<string, unknown>;
|
||||
assert.ok(!("max_tokens" in posted), "must not fabricate max_tokens");
|
||||
assert.ok(!("max_completion_tokens" in posted), "must not fabricate max_completion_tokens");
|
||||
});
|
||||
|
||||
test("Command Code executor clamps an oversized client-supplied max_tokens to the endpoint ceiling", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) });
|
||||
return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]);
|
||||
};
|
||||
|
||||
// A client asking for more than the 200000 endpoint ceiling is clamped down
|
||||
// (not 400'd), mirroring the provider-driven clamp in antigravity.ts.
|
||||
const calls = captureFetch({});
|
||||
// A client asking for more than the 200000 endpoint ceiling is clamped down.
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }], max_tokens: 500000 },
|
||||
});
|
||||
assert.equal(calls[0].body.params.max_tokens, 200000);
|
||||
assert.equal((calls[0].body as Record<string, unknown>).max_tokens, 200000);
|
||||
});
|
||||
|
||||
test("Command Code executor honors a smaller client-provided max_tokens under the per-model cap", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) });
|
||||
return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]);
|
||||
};
|
||||
|
||||
test("Command Code executor honors a smaller client-provided max_tokens", async () => {
|
||||
const calls = captureFetch({});
|
||||
await getExecutor("command-code").execute({
|
||||
model: "zai-org/GLM-5.1",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }], max_tokens: 2048 },
|
||||
});
|
||||
assert.equal(calls[0].body.params.max_tokens, 2048);
|
||||
assert.equal((calls[0].body as Record<string, unknown>).max_tokens, 2048);
|
||||
});
|
||||
|
||||
test("Command Code non-stream aggregation throws when the final error event lacks a trailing newline", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
`${JSON.stringify({ type: "text-delta", text: "Hello" })}\n${JSON.stringify({
|
||||
type: "error",
|
||||
error: { message: "boom" },
|
||||
})}`,
|
||||
{ status: 200, headers: { "Content-Type": "application/x-ndjson" } }
|
||||
);
|
||||
|
||||
await assert.rejects(async () => {
|
||||
await getExecutor("command-code").execute({
|
||||
test("Command Code stream preserves the upstream OpenAI usage chunk (passthrough)", async () => {
|
||||
const sse =
|
||||
openAiSse({
|
||||
id: "c1",
|
||||
object: "chat.completion.chunk",
|
||||
model: "gpt-5.4-mini",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
}, /boom/);
|
||||
});
|
||||
|
||||
test("Command Code usage chunk surfaces cache_read and no_cache for the stream pipeline", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream([
|
||||
{ type: "text-delta", text: "Hi" },
|
||||
{
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
totalUsage: {
|
||||
inputTokens: 10,
|
||||
inputTokenDetails: { noCacheTokens: 6, cacheReadTokens: 4 },
|
||||
outputTokens: 6,
|
||||
},
|
||||
choices: [{ index: 0, delta: { content: "Hi" } }],
|
||||
}) +
|
||||
openAiSse({
|
||||
id: "c1",
|
||||
object: "chat.completion.chunk",
|
||||
model: "gpt-5.4-mini",
|
||||
choices: [],
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
prompt_tokens_details: { cached_tokens: 4 },
|
||||
completion_tokens: 6,
|
||||
completion_tokens_details: { reasoning_tokens: 1 },
|
||||
total_tokens: 16,
|
||||
},
|
||||
]);
|
||||
}) +
|
||||
"data: [DONE]\n\n";
|
||||
globalThis.fetch = async () =>
|
||||
new Response(sse, { status: 200, headers: { "Content-Type": "text/event-stream" } });
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
@@ -532,260 +357,11 @@ test("Command Code usage chunk surfaces cache_read and no_cache for the stream p
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
|
||||
const sse = await response.text();
|
||||
const chunks = parseSsePayloads(sse);
|
||||
const usageChunk = chunks.find(
|
||||
(chunk) => Array.isArray(chunk.choices) && chunk.choices.length === 0
|
||||
);
|
||||
assert.ok(usageChunk, "expected a usage-only chunk (choices: []) in the stream");
|
||||
|
||||
// The usage-only chunk feeds stream.ts's extractUsage, which surfaces
|
||||
// cache_read_input_tokens / no_cache_tokens into the [USAGE] line.
|
||||
const { extractUsage } = await import("../../open-sse/utils/usageTracking.ts");
|
||||
const extracted = extractUsage(usageChunk);
|
||||
assert.ok(extracted, "extractUsage should recognize the usage-only chunk");
|
||||
assert.equal(extracted.prompt_tokens, 10);
|
||||
assert.equal(extracted.completion_tokens, 6);
|
||||
assert.equal(extracted.cache_read_input_tokens, 4);
|
||||
assert.equal(extracted.no_cache_tokens, 6);
|
||||
});
|
||||
|
||||
test("Command Code stream emits a usage-only chunk with actual tokens before [DONE]", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream([
|
||||
{ type: "text-delta", text: "Hi" },
|
||||
{
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
totalUsage: {
|
||||
inputTokens: 10,
|
||||
inputTokenDetails: { cacheReadTokens: 4, cacheCreationTokens: 2 },
|
||||
outputTokens: 6,
|
||||
reasoningTokenDetails: { reasoningTokens: 1 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
stream: true,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
|
||||
const sse = await response.text();
|
||||
const chunks = parseSsePayloads(sse);
|
||||
|
||||
// Find the usage-only chunk: choices must be [] and usage must carry the
|
||||
// actual upstream numbers. prompt_tokens = inputTokens (10) — cacheRead 4 is
|
||||
// already included in that 10, so it is reported separately, NOT re-added.
|
||||
const usageChunk = chunks.find(
|
||||
(chunk) => Array.isArray(chunk.choices) && chunk.choices.length === 0
|
||||
);
|
||||
assert.ok(usageChunk, "expected a usage-only chunk (choices: []) in the stream");
|
||||
assert.deepEqual(usageChunk.usage, {
|
||||
prompt_tokens: 10,
|
||||
prompt_tokens_details: { cached_tokens: 4 },
|
||||
completion_tokens: 6,
|
||||
completion_tokens_details: { reasoning_tokens: 1 },
|
||||
total_tokens: 16,
|
||||
cache_read_input_tokens: 4,
|
||||
reasoning_tokens: 1,
|
||||
});
|
||||
// The usage chunk must come before the [DONE] marker.
|
||||
assert.match(sse, /"usage":/);
|
||||
const doneIndex = sse.indexOf("data: [DONE]");
|
||||
const usageIndex = sse.indexOf(`"choices":[]`);
|
||||
assert.ok(usageIndex > -1 && usageIndex < doneIndex, "usage chunk must precede [DONE]");
|
||||
});
|
||||
|
||||
test("Command Code non-stream usage keeps inputTokens as prompt_tokens and reports cache separately", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream(
|
||||
[
|
||||
{ type: "text-delta", text: "ok" },
|
||||
{
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
totalUsage: {
|
||||
inputTokens: 5,
|
||||
inputTokenDetails: { noCacheTokens: 2, cacheReadTokens: 3 },
|
||||
outputTokens: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
{ sse: true }
|
||||
);
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
assert.deepEqual(json.usage, {
|
||||
prompt_tokens: 5,
|
||||
prompt_tokens_details: { cached_tokens: 3 },
|
||||
completion_tokens: 2,
|
||||
completion_tokens_details: { reasoning_tokens: 0 },
|
||||
total_tokens: 7,
|
||||
cache_read_input_tokens: 3,
|
||||
no_cache_tokens: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("Command Code preserves finish-step usage through a finish without totalUsage", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream([
|
||||
{ type: "text-delta", text: "Hi" },
|
||||
{
|
||||
type: "finish-step",
|
||||
usage: {
|
||||
inputTokens: 7308,
|
||||
inputTokenDetails: { noCacheTokens: 27, cacheReadTokens: 7281 },
|
||||
outputTokens: 177,
|
||||
outputTokenDetails: { textTokens: 12, reasoningTokens: 165 },
|
||||
totalTokens: 7485,
|
||||
},
|
||||
},
|
||||
{ type: "finish", finishReason: "stop", totalUsage: null },
|
||||
]);
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
stream: true,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
const sse = await response.text();
|
||||
const chunks = parseSsePayloads(sse);
|
||||
const usageChunk = chunks.find(
|
||||
(chunk) => Array.isArray(chunk.choices) && chunk.choices.length === 0
|
||||
);
|
||||
|
||||
assert.deepEqual(usageChunk?.usage, {
|
||||
prompt_tokens: 7308,
|
||||
prompt_tokens_details: { cached_tokens: 7281 },
|
||||
completion_tokens: 177,
|
||||
completion_tokens_details: { reasoning_tokens: 165 },
|
||||
total_tokens: 7485,
|
||||
cache_read_input_tokens: 7281,
|
||||
no_cache_tokens: 27,
|
||||
reasoning_tokens: 165,
|
||||
});
|
||||
|
||||
const completed = (await responsesFromChatSse(sse)).find(
|
||||
(event) => event.event === "response.completed"
|
||||
);
|
||||
assert.deepEqual(completed?.data.response.usage, {
|
||||
input_tokens: 7308,
|
||||
input_tokens_details: { cached_tokens: 7281 },
|
||||
output_tokens: 177,
|
||||
output_tokens_details: { reasoning_tokens: 165 },
|
||||
total_tokens: 7485,
|
||||
});
|
||||
});
|
||||
|
||||
test("Command Code accepts OpenAI-style usage aliases with absent optional details", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream([
|
||||
{
|
||||
type: "finish-step",
|
||||
usage: {
|
||||
prompt_tokens: 11,
|
||||
prompt_tokens_details: { cached_tokens: 4 },
|
||||
completion_tokens: 5,
|
||||
completion_tokens_details: { reasoning_tokens: 2 },
|
||||
total_tokens: 16,
|
||||
},
|
||||
},
|
||||
{ type: "finish", finishReason: "stop" },
|
||||
]);
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
stream: true,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
const sse = await response.text();
|
||||
const usageChunk = parseSsePayloads(sse).find(
|
||||
(chunk) => Array.isArray(chunk.choices) && chunk.choices.length === 0
|
||||
);
|
||||
assert.deepEqual(usageChunk?.usage, {
|
||||
prompt_tokens: 11,
|
||||
prompt_tokens_details: { cached_tokens: 4 },
|
||||
completion_tokens: 5,
|
||||
completion_tokens_details: { reasoning_tokens: 2 },
|
||||
total_tokens: 16,
|
||||
cache_read_input_tokens: 4,
|
||||
reasoning_tokens: 2,
|
||||
});
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream([
|
||||
{ type: "finish-step", usage: { inputTokens: 4, outputTokens: 3, totalTokens: 7 } },
|
||||
{ type: "finish", finishReason: "stop", totalUsage: null },
|
||||
]);
|
||||
const fallback = await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
stream: true,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
const fallbackSse = await fallback.response.text();
|
||||
const completed = (await responsesFromChatSse(fallbackSse)).find(
|
||||
(event) => event.event === "response.completed"
|
||||
);
|
||||
assert.deepEqual(completed?.data.response.usage, {
|
||||
input_tokens: 4,
|
||||
input_tokens_details: { cached_tokens: 0 },
|
||||
output_tokens: 3,
|
||||
output_tokens_details: { reasoning_tokens: 0 },
|
||||
total_tokens: 7,
|
||||
});
|
||||
});
|
||||
|
||||
test("Command Code preserves tool-call streaming while finalizing finish-step usage", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream([
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call_1",
|
||||
toolName: "lookup",
|
||||
input: { query: "hello" },
|
||||
},
|
||||
{ type: "finish-step", usage: { inputTokens: 3, outputTokens: 2, totalTokens: 5 } },
|
||||
{ type: "finish", finishReason: "tool-calls" },
|
||||
]);
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
stream: true,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Hi" }] },
|
||||
});
|
||||
const sse = await response.text();
|
||||
assert.ok(
|
||||
parseSsePayloads(sse).some(
|
||||
(chunk) => chunk.choices?.[0]?.delta?.tool_calls?.[0]?.id === "call_1"
|
||||
)
|
||||
);
|
||||
|
||||
const completed = (await responsesFromChatSse(sse)).find(
|
||||
(event) => event.event === "response.completed"
|
||||
);
|
||||
assert.equal(
|
||||
completed?.data.response.output?.some((item) => item.type === "function_call"),
|
||||
true
|
||||
);
|
||||
assert.deepEqual(completed?.data.response.usage, {
|
||||
input_tokens: 3,
|
||||
input_tokens_details: { cached_tokens: 0 },
|
||||
output_tokens: 2,
|
||||
output_tokens_details: { reasoning_tokens: 0 },
|
||||
total_tokens: 5,
|
||||
});
|
||||
});
|
||||
const text = await response.text();
|
||||
// The upstream OpenAI usage chunk passes through unchanged, including the
|
||||
// standard OpenAI usage shape the stream pipeline already understands.
|
||||
assert.ok(text.includes('"prompt_tokens":10'));
|
||||
assert.ok(text.includes('"cached_tokens":4'));
|
||||
assert.ok(text.includes('"reasoning_tokens":1'));
|
||||
assert.ok(text.includes("data: [DONE]"));
|
||||
});
|
||||
@@ -34,7 +34,7 @@ test.after(() => {
|
||||
core.resetDbInstance();
|
||||
});
|
||||
|
||||
async function captureParams(body: Record<string, unknown>): Promise<FetchCall> {
|
||||
async function captureBody(body: Record<string, unknown>): Promise<FetchCall> {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url: any, init: any = {}) => {
|
||||
calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) });
|
||||
@@ -50,27 +50,27 @@ async function captureParams(body: Record<string, unknown>): Promise<FetchCall>
|
||||
}
|
||||
|
||||
test("Command Code omits max_tokens when the client sends max_tokens: -1 (#5166)", async () => {
|
||||
const call = await captureParams({ max_tokens: -1 });
|
||||
const call = await captureBody({ max_tokens: -1 });
|
||||
assert.ok(
|
||||
!("max_tokens" in call.body.params),
|
||||
`max_tokens:-1 must be omitted, got params.max_tokens=${call.body.params.max_tokens}`
|
||||
!("max_tokens" in call.body),
|
||||
`max_tokens:-1 must be omitted, got max_tokens=${call.body.max_tokens}`
|
||||
);
|
||||
});
|
||||
|
||||
test("Command Code omits max_tokens when the client sends max_completion_tokens: -1 (#5166)", async () => {
|
||||
const call = await captureParams({ max_completion_tokens: -1 });
|
||||
const call = await captureBody({ max_completion_tokens: -1 });
|
||||
assert.ok(
|
||||
!("max_tokens" in call.body.params),
|
||||
`max_completion_tokens:-1 must be omitted, got params.max_tokens=${call.body.params.max_tokens}`
|
||||
!("max_tokens" in call.body),
|
||||
`max_completion_tokens:-1 must be omitted, got max_tokens=${call.body.max_tokens}`
|
||||
);
|
||||
});
|
||||
|
||||
test("Command Code omits max_tokens when the client sends 0 (#5166)", async () => {
|
||||
const call = await captureParams({ max_tokens: 0 });
|
||||
assert.ok(!("max_tokens" in call.body.params), "max_tokens:0 must be omitted");
|
||||
const call = await captureBody({ max_tokens: 0 });
|
||||
assert.ok(!("max_tokens" in call.body), "max_tokens:0 must be omitted");
|
||||
});
|
||||
|
||||
test("Command Code still honors a positive client max_tokens after the #5166 fix", async () => {
|
||||
const call = await captureParams({ max_tokens: 2048 });
|
||||
assert.equal(call.body.params.max_tokens, 2048);
|
||||
});
|
||||
const call = await captureBody({ max_tokens: 2048 });
|
||||
assert.equal(call.body.max_tokens, 2048);
|
||||
});
|
||||
@@ -1,14 +1,13 @@
|
||||
/**
|
||||
* Regression test for #5166 (user-content-array 400 on Command Code / deepseek-v4-pro).
|
||||
* #5166 (user-content-array 400 on Command Code / deepseek-v4-pro) context.
|
||||
*
|
||||
* When a client sends a user message whose `content` is an array of content parts
|
||||
* (e.g. [{type:"text",text:"Hello"},{type:"text",text:"World"}]), the raw array
|
||||
* must NOT reach the Command Code upstream — it requires user content to be a plain
|
||||
* string. The executor must normalise the array to a string before posting.
|
||||
*
|
||||
* NOTE: this file covers ONLY the user-content-array/400 symptom of #5166.
|
||||
* The 0-output-token symptom on mimo-v2.5-pro (reasoning-only models) is tracked
|
||||
* separately and is NOT addressed here.
|
||||
* The original regression was that a user message whose `content` was an array of
|
||||
* content parts reached the CLI-only /alpha/generate endpoint, which required
|
||||
* user content to be a plain string. Since #10265 the executor posts to the
|
||||
* documented /provider/v1/chat/completions endpoint, which natively speaks the
|
||||
* OpenAI chat.completions format — array content (text + image_url parts) is
|
||||
* valid there and passes through unchanged. These tests pin that OpenAI-shaped
|
||||
* passthrough.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
@@ -26,9 +25,8 @@ const core = await import("../../src/lib/db/core.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
function commandCodeStream(lines: unknown[]) {
|
||||
const text = lines.map((l) => JSON.stringify(l)).join("\n") + "\n";
|
||||
return new Response(text, { status: 200, headers: { "Content-Type": "application/x-ndjson" } });
|
||||
function okResponse() {
|
||||
return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
@@ -41,155 +39,100 @@ test.afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────────────────
|
||||
// ── helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
type FetchCall = { url: string; init: Record<string, unknown>; body: Record<string, unknown> };
|
||||
|
||||
function captureFetch(response: Response) {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
||||
calls.push({ url: String(url), init: init as Record<string, unknown>, body: JSON.parse(String(init.body)) });
|
||||
calls.push({
|
||||
url: String(url),
|
||||
init: init as Record<string, unknown>,
|
||||
body: JSON.parse(String(init.body)),
|
||||
});
|
||||
return response;
|
||||
};
|
||||
return calls;
|
||||
}
|
||||
|
||||
// ── failing tests (before fix, user content is the raw array) ──────────────
|
||||
test("#5166 user message with multi-part array content passes through as an OpenAI array", async () => {
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Hello" },
|
||||
{ type: "text", text: "World" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
test(
|
||||
"#5166 user message with multi-part array content is flattened to a string (#5166)",
|
||||
async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
const userMsg = (calls[0].body.messages as Record<string, unknown>[])[0];
|
||||
// OpenAI array content is valid on /provider/v1 — forwarded as-is.
|
||||
assert.ok(Array.isArray(userMsg.content), "array content forwarded (no CLI flattening)");
|
||||
const parts = userMsg.content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2);
|
||||
assert.equal(parts[0].text, "Hello");
|
||||
assert.equal(parts[1].text, "World");
|
||||
});
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Hello" },
|
||||
{ type: "text", text: "World" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
test("#5166 user message with single text-part array passes through", async () => {
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [{ role: "user", content: [{ type: "text", text: "Hi there" }] }],
|
||||
},
|
||||
});
|
||||
const userMsg = (calls[0].body.messages as Record<string, unknown>[])[0];
|
||||
const parts = userMsg.content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 1);
|
||||
assert.equal(parts[0].text, "Hi there");
|
||||
});
|
||||
|
||||
const posted = calls[0].body;
|
||||
const userMsg = (posted.params as Record<string, unknown[]>).messages[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
test("#5166 user message with plain string content passes through unchanged", async () => {
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Plain string message" }] },
|
||||
});
|
||||
const userMsg = (calls[0].body.messages as Record<string, unknown>[])[0];
|
||||
assert.equal(userMsg.content, "Plain string message");
|
||||
});
|
||||
|
||||
// Must be a string — never an array — otherwise Command Code's upstream returns 400.
|
||||
assert.equal(
|
||||
typeof userMsg.content,
|
||||
"string",
|
||||
`user message content must be a string, got ${typeof userMsg.content}`
|
||||
);
|
||||
// Joined text parts with "\n"
|
||||
assert.equal(userMsg.content, "Hello\nWorld");
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"#5166 user message with single text-part array is flattened to a plain string",
|
||||
async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Hi there" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const posted = calls[0].body;
|
||||
const userMsg = (posted.params as Record<string, unknown[]>).messages[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(typeof userMsg.content, "string");
|
||||
assert.equal(userMsg.content, "Hi there");
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"#5166 user message with plain string content passes through unchanged (no regression)",
|
||||
async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Plain string message",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const posted = calls[0].body;
|
||||
const userMsg = (posted.params as Record<string, unknown[]>).messages[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(typeof userMsg.content, "string");
|
||||
assert.equal(userMsg.content, "Plain string message");
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"#5166 user message with mixed parts (text + image_url) keeps only text parts",
|
||||
async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe this:" },
|
||||
{ type: "image_url", image_url: { url: "https://example.com/img.png" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const posted = calls[0].body;
|
||||
const userMsg = (posted.params as Record<string, unknown[]>).messages[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(typeof userMsg.content, "string");
|
||||
// Only text parts extracted; image_url part is dropped (not a "text" type)
|
||||
assert.equal(userMsg.content, "Describe this:");
|
||||
}
|
||||
);
|
||||
test("#5166 user message with mixed parts (text + image_url) keeps all parts", async () => {
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe this:" },
|
||||
{ type: "image_url", image_url: { url: "https://example.com/img.png" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const userMsg = (calls[0].body.messages as Record<string, unknown>[])[0];
|
||||
const parts = userMsg.content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2, "text + image both preserved");
|
||||
assert.equal(parts[0].text, "Describe this:");
|
||||
assert.equal(parts[1].type, "image_url");
|
||||
});
|
||||
@@ -1,9 +1,13 @@
|
||||
/**
|
||||
* Vision / multimodal support tests for the Command Code executor.
|
||||
*
|
||||
* Verifies that vision-capable models (MiniMax M3, MiMo V2.5, Kimi K2, Qwen 3.x, GPT-5, Claude 3/4, Fable 5, Gemini 3.x, Stepfun, Fugu, etc.)
|
||||
* receive image parts in Command Code CLI format, while text-only
|
||||
* models strip images as before (no regression).
|
||||
* Since #10265 the executor posts to the documented /provider/v1/chat/completions
|
||||
* endpoint, which speaks the standard OpenAI chat.completions format. User image
|
||||
* content (OpenAI `image_url` parts and Anthropic Messages-style source blocks)
|
||||
* passes through unchanged — the endpoint natively understands both shapes, so
|
||||
* there is no CLI-specific conversion (and no CLI-wire image stripping) left to
|
||||
* verify. These tests pin that passthrough plus the #10809 wire-model
|
||||
* normalization, which still applies to /provider/v1.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
@@ -19,9 +23,8 @@ const core = await import("../../src/lib/db/core.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
function commandCodeStream(lines: unknown[]) {
|
||||
const text = lines.map((l) => JSON.stringify(l)).join("\n") + "\n";
|
||||
return new Response(text, { status: 200, headers: { "Content-Type": "application/x-ndjson" } });
|
||||
function okResponse() {
|
||||
return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
@@ -52,44 +55,28 @@ function captureFetch(response: Response) {
|
||||
}
|
||||
|
||||
function userContent(calls: FetchCall[]): unknown {
|
||||
return (
|
||||
(calls[0].body.params as Record<string, unknown[]>).messages as Record<string, unknown>[]
|
||||
)[0].content;
|
||||
return (calls[0].body.messages as Record<string, unknown>[])[0].content;
|
||||
}
|
||||
|
||||
function wireModel(calls: FetchCall[]): string {
|
||||
return calls[0].body.model as string;
|
||||
}
|
||||
|
||||
// ── wire model normalization (#10809) ────────────────────────────────
|
||||
//
|
||||
// Command Code's /alpha/generate endpoint serves most models under a
|
||||
// vendor-prefixed wire id and defaults an unprefixed id to the `anthropic:`
|
||||
// provider (403 "Model/provider not recognized: anthropic:<id>"). A bare id
|
||||
// reaches the executor when an operator sets a custom vision model in the
|
||||
// Vision Bridge picker (e.g. `command-code/mimo-v2.5`). The executor must
|
||||
// normalize to the documented vendor-prefixed wire form.
|
||||
|
||||
function wireModel(calls: FetchCall[]): string {
|
||||
return (calls[0].body.params as Record<string, unknown>).model as string;
|
||||
}
|
||||
|
||||
test("#10809: command-code/mimo-v2.5 wire model is normalized to xiaomi/mimo-v2.5", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "command-code/mimo-v2.5",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
model: "command-code/mimo-v2.5",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
},
|
||||
body: { model: "command-code/mimo-v2.5", messages: [{ role: "user", content: "hi" }] },
|
||||
});
|
||||
assert.equal(wireModel(calls), "xiaomi/mimo-v2.5");
|
||||
});
|
||||
|
||||
test("#10809: cmd/mimo-v2.5 (alias prefix) is also normalized", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "cmd/mimo-v2.5",
|
||||
stream: false,
|
||||
@@ -100,9 +87,7 @@ test("#10809: cmd/mimo-v2.5 (alias prefix) is also normalized", async () => {
|
||||
});
|
||||
|
||||
test("#10809: already vendor-prefixed wire ids pass through unchanged", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "command-code/deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
@@ -115,13 +100,10 @@ test("#10809: already vendor-prefixed wire ids pass through unchanged", async ()
|
||||
assert.equal(wireModel(calls), "deepseek/deepseek-v4-pro");
|
||||
});
|
||||
|
||||
// ── vision models: image parts preserved in CC CLI format ─────────────
|
||||
|
||||
test("vision model minimax-m3 preserves image_url part as CC CLI {type:image}", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
// ── image content passthrough (OpenAI /provider/v1 surface) ──────────
|
||||
|
||||
test("image_url parts pass through unchanged (text + image preserved)", async () => {
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "MiniMaxAI/MiniMax-M3",
|
||||
stream: false,
|
||||
@@ -132,451 +114,25 @@ test("vision model minimax-m3 preserves image_url part as CC CLI {type:image}",
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "What's in this?" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "data:image/png;base64,iVBORw0KGgo=" },
|
||||
},
|
||||
{ type: "image_url", image_url: { url: "data:image/png;base64,iVBORw0KGgo=" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content), "vision model user content must be an array");
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2);
|
||||
|
||||
// Text part preserved
|
||||
assert.equal(parts[0].type, "text");
|
||||
assert.equal(parts[0].text, "What's in this?");
|
||||
|
||||
// Image part converted to CC CLI format
|
||||
assert.equal(parts[1].type, "image");
|
||||
assert.equal(parts[1].image, "data:image/png;base64,iVBORw0KGgo=");
|
||||
const content = userContent(calls) as Record<string, unknown>[];
|
||||
assert.equal(content.length, 2);
|
||||
assert.equal(content[0].type, "text");
|
||||
assert.equal(content[1].type, "image_url", "image_url part preserved as-is");
|
||||
assert.equal(
|
||||
(content[1].image_url as { url: string }).url,
|
||||
"data:image/png;base64,iVBORw0KGgo="
|
||||
);
|
||||
});
|
||||
|
||||
test("vision model minimax-m3 preserves image_url with HTTP URL", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "minimax-m3",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/photo.jpg" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content));
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2);
|
||||
assert.equal(parts[1].type, "image");
|
||||
assert.equal(parts[1].image, "https://example.com/photo.jpg");
|
||||
});
|
||||
|
||||
test("vision model mimo-v2.5 preserves image parts", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "mimo-v2.5",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Analyze" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/img.png" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content));
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2);
|
||||
assert.equal(parts[1].type, "image");
|
||||
});
|
||||
|
||||
test("vision model mimo-v2.5-pro is text-only (no image parts)", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "mimo-v2.5-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Hi" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/img.png" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
// mimo-v2.5-pro is text-only — content must be flattened to a plain string
|
||||
assert.equal(typeof content, "string");
|
||||
assert.equal(content, "Hi");
|
||||
});
|
||||
|
||||
test("vision model mimo-v2-omni preserves image parts", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "mimo-v2-omni",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Check" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "data:image/jpeg;base64,/9j/4AAQ=" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content));
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2);
|
||||
assert.equal(parts[1].type, "image");
|
||||
assert.equal(parts[1].image, "data:image/jpeg;base64,/9j/4AAQ=");
|
||||
});
|
||||
|
||||
// ── non-vision models: images still stripped (no regression) ──────────
|
||||
|
||||
test("text-only model deepseek-v4-pro strips image_url parts", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Hello" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/img.png" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
// Non-vision model: content is a plain string, images stripped
|
||||
assert.equal(typeof content, "string");
|
||||
assert.equal(content, "Hello");
|
||||
});
|
||||
|
||||
test("text-only model deepseek-v4-flash strips image_url parts (no regression)", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-flash",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Text only" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "data:image/png;base64,AAA=" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.equal(typeof content, "string");
|
||||
assert.equal(content, "Text only");
|
||||
});
|
||||
|
||||
// ── edge cases ────────────────────────────────────────────────────────
|
||||
|
||||
test("vision model with only image content emits empty text fallback", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "minimax-m3",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "data:image/png;base64,iVBOR=" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content));
|
||||
const parts = content as Record<string, unknown>[];
|
||||
// Single image part preserved — no empty text injected because
|
||||
// the image itself keeps content non-empty.
|
||||
assert.equal(parts.length, 1);
|
||||
assert.equal(parts[0].type, "image");
|
||||
assert.equal(parts[0].image, "data:image/png;base64,iVBOR=");
|
||||
});
|
||||
|
||||
test("vision model passes plain string content through unchanged", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "minimax-m3",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [{ role: "user", content: "Plain string message" }],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.equal(typeof content, "string");
|
||||
assert.equal(content, "Plain string message");
|
||||
});
|
||||
|
||||
test("vision model honors body.model rewrite for vision detection", async () => {
|
||||
// #5166 scenario: body.model overwrites the execute model arg.
|
||||
// Vision detection must use the rewritten model id.
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
// execute() gets a non-vision combo model, body.model rewrites to a vision model
|
||||
await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
model: "MiniMaxAI/MiniMax-M3",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/img.png" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
// body.model = MiniMax-M3 (vision) → images preserved
|
||||
assert.ok(Array.isArray(content));
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2);
|
||||
assert.equal(parts[1].type, "image");
|
||||
});
|
||||
|
||||
test("vision model with multiple image parts preserves all of them", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "minimax-m3",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Compare" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/a.jpg" },
|
||||
},
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/b.jpg" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content));
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 3);
|
||||
assert.equal(parts[0].type, "text");
|
||||
assert.equal(parts[1].type, "image");
|
||||
assert.equal(parts[1].image, "https://example.com/a.jpg");
|
||||
assert.equal(parts[2].type, "image");
|
||||
assert.equal(parts[2].image, "https://example.com/b.jpg");
|
||||
});
|
||||
|
||||
test("vision model with image_url as plain string (no object wrapper) still works", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "minimax-m3",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Look" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: "https://example.com/img.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content));
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2);
|
||||
assert.equal(parts[1].type, "image");
|
||||
assert.equal(parts[1].image, "https://example.com/img.png");
|
||||
});
|
||||
|
||||
// ── CC vision models (Command Code docs registry) ──────────────────
|
||||
|
||||
const VISION_CASES = [
|
||||
["Kimi K2.6", "moonshotai/Kimi-K2.6"],
|
||||
["Kimi K2.7 Code", "moonshotai/Kimi-K2.7-Code"],
|
||||
["Kimi K2.5", "moonshotai/Kimi-K2.5"],
|
||||
["Qwen 3.6 Plus", "Qwen/Qwen3.6-Plus"],
|
||||
["Qwen 3.7 Plus", "Qwen/Qwen3.7-Plus"],
|
||||
["Step 3.7 Flash", "stepfun/Step-3.7-Flash"],
|
||||
["GPT-5.5", "gpt-5.5"],
|
||||
["GPT-5.4", "gpt-5.4"],
|
||||
["GPT-5.3 Codex", "gpt-5.3-codex"],
|
||||
["GPT-5.4 Mini", "gpt-5.4-mini"],
|
||||
["Claude Fable 5", "claude-fable-5"],
|
||||
["Sakana Fugu Ultra", "sakana/fugu-ultra"],
|
||||
["Claude Opus 4.7 (isVisionModelId)", "claude-opus-4-7"],
|
||||
["Claude Sonnet 4.6 (isVisionModelId)", "claude-sonnet-4-6"],
|
||||
["Gemini 3.5 Flash (isVisionModelId)", "google/gemini-3.5-flash"],
|
||||
];
|
||||
|
||||
for (const [name, model] of VISION_CASES) {
|
||||
test(`vision model ${name} preserves image parts`, async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
await getExecutor("command-code").execute({
|
||||
model,
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Check" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/img.png" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content), `${name} user content must be an array`);
|
||||
const parts = content;
|
||||
assert.equal(parts.length, 2);
|
||||
assert.equal(parts[1].type, "image");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Anthropic-shaped image blocks (Zoo Code / Claude-Code-compatible clients) ──
|
||||
|
||||
test("vision model mimo-v2.5 preserves Anthropic source.base64 image block", async () => {
|
||||
// Zoo Code sends Messages-API-shaped content blocks to the OpenAI
|
||||
// /v1/chat/completions surface: { type:"image", source:{ base64 } }.
|
||||
// The vision-bridge guardrail skips vision-capable models (cmd/xiaomi/mimo-v2.5
|
||||
// resolves supportsVision=true via the mimo-v2.5 leaf spec), so the raw block
|
||||
// must survive to the executor and be converted to CC CLI { type:"image" }.
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
test("Anthropic Messages-style source image blocks pass through unchanged", async () => {
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "xiaomi/mimo-v2.5",
|
||||
stream: false,
|
||||
@@ -601,24 +157,15 @@ test("vision model mimo-v2.5 preserves Anthropic source.base64 image block", asy
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content), "user content must be an array");
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2, "text + image parts preserved");
|
||||
assert.equal(parts[0].type, "text");
|
||||
assert.equal(parts[1].type, "image");
|
||||
assert.equal(
|
||||
parts[1].image,
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
|
||||
"base64 payload is rebuilt into a CC CLI data URL"
|
||||
);
|
||||
const content = userContent(calls) as Record<string, unknown>[];
|
||||
assert.equal(content.length, 2, "text + image parts preserved");
|
||||
assert.equal(content[1].type, "image");
|
||||
assert.equal((content[1].source as { type: string }).type, "base64");
|
||||
assert.equal((content[1].source as { data: string }).data, "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==");
|
||||
});
|
||||
|
||||
test("vision model preserves Anthropic source.url image block", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
test("Anthropic source.url image block passes through unchanged", async () => {
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "xiaomi/mimo-v2.5",
|
||||
stream: false,
|
||||
@@ -629,31 +176,23 @@ test("vision model preserves Anthropic source.url image block", async () => {
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Look" },
|
||||
{
|
||||
type: "image",
|
||||
source: { type: "url", url: "https://example.com/img.png" },
|
||||
},
|
||||
{ type: "image", source: { type: "url", url: "https://example.com/img.png" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content));
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2);
|
||||
assert.equal(parts[1].type, "image");
|
||||
assert.equal(parts[1].image, "https://example.com/img.png");
|
||||
const content = userContent(calls) as Record<string, unknown>[];
|
||||
assert.equal(content.length, 2);
|
||||
assert.equal(content[1].type, "image");
|
||||
assert.deepEqual(content[1].source, { type: "url", url: "https://example.com/img.png" });
|
||||
});
|
||||
|
||||
test("text-only model deepseek-v4-flash strips Anthropic source.base64 image block", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
test("multiple image parts are all preserved", async () => {
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "deepseek/deepseek-v4-flash",
|
||||
model: "minimax-m3",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
@@ -661,44 +200,41 @@ test("text-only model deepseek-v4-flash strips Anthropic source.base64 image blo
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Text only" },
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: "image/png",
|
||||
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
|
||||
},
|
||||
},
|
||||
{ type: "text", text: "Compare" },
|
||||
{ type: "image_url", image_url: { url: "https://example.com/a.jpg" } },
|
||||
{ type: "image_url", image_url: { url: "https://example.com/b.jpg" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls) as Record<string, unknown>[];
|
||||
assert.equal(content.length, 3);
|
||||
assert.equal(content[1].type, "image_url");
|
||||
assert.equal(content[2].type, "image_url");
|
||||
});
|
||||
|
||||
test("plain string content passes through unchanged", async () => {
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "minimax-m3",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Plain string message" }] },
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
// Text-only model: content flattened to plain string, image stripped.
|
||||
assert.equal(typeof content, "string");
|
||||
assert.equal(content, "Text only");
|
||||
assert.equal(content, "Plain string message");
|
||||
});
|
||||
|
||||
// ── conservative vision family lock (gpt-5.4-mini / gpt-5.3-codex) ─────
|
||||
//
|
||||
// These two ids stay INSIDE the `/gpt-5/` vision family: both accept image
|
||||
// input on the OpenAI API, and there is no verified Command Code backend data
|
||||
// marking them text-only. These tests pin that conservative executor behavior
|
||||
// so a future "narrow the regex" change cannot silently strip images from models
|
||||
// that can see them (the #4071 regression class). The #10703 Vision Bridge
|
||||
// candidate-list fix lives in the shared capability resolution
|
||||
// (KNOWN_TEXT_ONLY_DESPITE_SYNC), NOT in the executor's wire transform.
|
||||
|
||||
test("gpt-5.4-mini keeps image parts (conservative vision family lock)", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
test("text-only model still forwards image parts (passthrough, no CLI stripping)", async () => {
|
||||
// The /provider/v1 OpenAI surface accepts image content for any model id; the
|
||||
// executor forwards content untouched, so there is no text-only stripping.
|
||||
const calls = captureFetch(okResponse());
|
||||
await getExecutor("command-code").execute({
|
||||
model: "gpt-5.4-mini",
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
@@ -706,54 +242,15 @@ test("gpt-5.4-mini keeps image parts (conservative vision family lock)", async (
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "What's in this?" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/img.png" },
|
||||
},
|
||||
{ type: "text", text: "Hello" },
|
||||
{ type: "image_url", image_url: { url: "https://example.com/img.png" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content), "gpt-5.4-mini must be treated as vision-capable");
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2);
|
||||
assert.equal(parts[1].type, "image");
|
||||
assert.equal(parts[1].image, "https://example.com/img.png");
|
||||
});
|
||||
|
||||
test("gpt-5.3-codex keeps image parts (conservative vision family lock)", async () => {
|
||||
const calls = captureFetch(
|
||||
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
|
||||
);
|
||||
|
||||
await getExecutor("command-code").execute({
|
||||
model: "gpt-5.3-codex",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe" },
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/img.png" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const content = userContent(calls);
|
||||
assert.ok(Array.isArray(content), "gpt-5.3-codex must be treated as vision-capable");
|
||||
const parts = content as Record<string, unknown>[];
|
||||
assert.equal(parts.length, 2);
|
||||
assert.equal(parts[1].type, "image");
|
||||
assert.equal(parts[1].image, "https://example.com/img.png");
|
||||
});
|
||||
const content = userContent(calls) as Record<string, unknown>[];
|
||||
assert.equal(content.length, 2, "content array forwarded unchanged");
|
||||
assert.equal(content[1].type, "image_url");
|
||||
});
|
||||
@@ -15,7 +15,10 @@ class MockM365WebSocket {
|
||||
closed = false;
|
||||
listeners = new Map<string, Listener[]>();
|
||||
|
||||
constructor(public url: string, public options: unknown) {
|
||||
constructor(
|
||||
public url: string,
|
||||
public options: unknown
|
||||
) {
|
||||
MockM365WebSocket.instances.push(this);
|
||||
queueMicrotask(() => this.emit("open"));
|
||||
}
|
||||
@@ -144,30 +147,55 @@ test("#7870: enterprise-tier chat invocation defaults tone to Magic", async () =
|
||||
assert.equal(invocationArgs.tone, "Magic");
|
||||
});
|
||||
|
||||
test("#10718: individual (no tier) chat invocation carries the recaptured 2026-08 shape", async () => {
|
||||
test("2026-08-21: individual (no tier) chat invocation carries the recaptured shape", async () => {
|
||||
const invocationArgs = await sendChatInvocation(undefined);
|
||||
const optionsSets = invocationArgs.optionsSets as string[];
|
||||
// The 25-entry consumer/MSA set (enable_msa_user, pdnascan, …) is gone from
|
||||
// the wire — the stale set was part of the silently-dropped shape.
|
||||
// The pre-#10718 25-entry consumer/MSA set (enable_msa_user, pdnascan, …) is
|
||||
// still gone from the wire — only the 2026-08-21 34-entry set is current.
|
||||
assert.ok(!optionsSets.includes("enable_msa_user"));
|
||||
assert.ok(optionsSets.includes("enable_gg_gpt"));
|
||||
// The browser sends tone:"magic" (lowercase) on the individual/EDU surface.
|
||||
assert.equal(invocationArgs.tone, "magic");
|
||||
// The individual/consumer surface now sends tone:"Magic" (capitalized) —
|
||||
// the #10718 lowercase "magic" is stale.
|
||||
assert.equal(invocationArgs.tone, "Magic");
|
||||
assert.deepEqual(invocationArgs.allowedMessageTypes, [
|
||||
"Chat",
|
||||
"Suggestion",
|
||||
"InternalSearchQuery",
|
||||
"Disengaged",
|
||||
"Progress",
|
||||
"EndOfRequest",
|
||||
"InternalLoaderMessage",
|
||||
"Progress",
|
||||
"GeneratedCode",
|
||||
"RenderCardRequest",
|
||||
"AdsQuery",
|
||||
"SemanticSerp",
|
||||
"GenerateContentQuery",
|
||||
"GenerateGraphicArt",
|
||||
"SearchQuery",
|
||||
"ConfirmationCard",
|
||||
"AuthError",
|
||||
"DeveloperLogs",
|
||||
"TriggerPlugin",
|
||||
"HintInvocation",
|
||||
"MemoryUpdate",
|
||||
"EndOfRequest",
|
||||
"TriggerConfirmation",
|
||||
"ResumeInvokeAction",
|
||||
"ResumeUserInputRequest",
|
||||
"TriggerUserInputRequest",
|
||||
"EscapeHatch",
|
||||
"TriggerPluginAuth",
|
||||
"ResumePluginAuth",
|
||||
"SideBySide",
|
||||
"ReferencesListComplete",
|
||||
"SwitchRespondingEndpoint",
|
||||
]);
|
||||
});
|
||||
|
||||
test("#10718: EDU-tier chat invocation carries the same recaptured shape", async () => {
|
||||
test("2026-08-21: EDU-tier chat invocation carries the same recaptured shape", async () => {
|
||||
const invocationArgs = await sendChatInvocation("edu");
|
||||
const optionsSets = invocationArgs.optionsSets as string[];
|
||||
assert.ok(!optionsSets.includes("enable_msa_user"));
|
||||
assert.equal(invocationArgs.tone, "magic");
|
||||
assert.equal(invocationArgs.tone, "Magic");
|
||||
});
|
||||
|
||||
test("#8971: enterprise-tier chat invocation must send disconnectBehavior=continue", async () => {
|
||||
@@ -179,11 +207,11 @@ test("#8971: enterprise-tier chat invocation must send disconnectBehavior=contin
|
||||
);
|
||||
});
|
||||
|
||||
test("#8971/#10718: individual (no tier) chat invocation omits disconnectBehavior (not on the 2026-08 wire)", async () => {
|
||||
test("2026-08-21: individual (no tier) chat invocation also sends disconnectBehavior=continue (now on every tier)", async () => {
|
||||
const invocationArgs = await sendChatInvocation(undefined);
|
||||
assert.equal(
|
||||
invocationArgs.disconnectBehavior,
|
||||
undefined,
|
||||
`individual-tier invocation must omit disconnectBehavior; got ${JSON.stringify(invocationArgs.disconnectBehavior)}`
|
||||
"continue",
|
||||
`individual-tier invocation must carry disconnectBehavior="continue"; got ${JSON.stringify(invocationArgs.disconnectBehavior)}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ test("#10718: metricsFrame emits the exact bytes observed in the browser capture
|
||||
|
||||
// ── Recaptured invocation shape ────────────────────────────────────────────
|
||||
|
||||
test("#10718: buildChatInvocation matches the recaptured arguments[0] key set", () => {
|
||||
test("2026-08-21: buildChatInvocation matches the recaptured arguments[0] key set", () => {
|
||||
const arg = buildChatInvocation({
|
||||
text: "Say OK in one word.",
|
||||
traceId: "11111111-1111-1111-1111-111111111111",
|
||||
@@ -50,19 +50,25 @@ test("#10718: buildChatInvocation matches the recaptured arguments[0] key set",
|
||||
conversationId: "44444444-4444-4444-4444-444444444444",
|
||||
}).arguments[0] as Record<string, unknown>;
|
||||
|
||||
// Exact key set from the capture — additions AND omissions are both pinned,
|
||||
// because the stale keys are exactly what got the shape dropped.
|
||||
// Exact key set from the 2026-08-21 capture (issue: individual/consumer M365
|
||||
// Copilot calls got only SignalR keepalive pings and no type:1 update at all
|
||||
// — "Stream ended before producing a non-ping SSE event" client-side). The
|
||||
// #10718 shape below is missing exactly the keys this capture added.
|
||||
assert.deepEqual(Object.keys(arg).sort(), [
|
||||
"allowedMessageTypes",
|
||||
"clientCorrelationId",
|
||||
"clientInfo",
|
||||
"conversationId",
|
||||
"disconnectBehavior",
|
||||
"extraExtensionParameters",
|
||||
"isSbsSupported",
|
||||
"isStartOfSession",
|
||||
"message",
|
||||
"options",
|
||||
"optionsSets",
|
||||
"plugins",
|
||||
"productThreadType",
|
||||
"renderReferencesBehindEOS",
|
||||
"sessionId",
|
||||
"sliceIds",
|
||||
"source",
|
||||
@@ -73,13 +79,32 @@ test("#10718: buildChatInvocation matches the recaptured arguments[0] key set",
|
||||
"traceId",
|
||||
]);
|
||||
assert.equal(arg.productThreadType, "Office");
|
||||
assert.deepEqual(arg.clientInfo, { clientAppName: "Office", clientPlatform: "mcmcopilot-web" });
|
||||
assert.deepEqual(arg.clientInfo, {
|
||||
clientAppName: "Office",
|
||||
clientPlatform: "mcmcopilot-web",
|
||||
clientEntrypoint: "mcmcopilot-officeweb",
|
||||
clientSessionId: "22222222-2222-2222-2222-222222222222",
|
||||
ProductCategory: "Chat",
|
||||
clientAppType: "Web",
|
||||
productEntryPoint: "ChatPanel",
|
||||
deviceOS: "Windows",
|
||||
deviceType: "Desktop",
|
||||
clientPlatformVersion: "10",
|
||||
});
|
||||
assert.equal(arg.conversationId, "44444444-4444-4444-4444-444444444444");
|
||||
assert.equal(arg.toolChoice, null);
|
||||
assert.equal(arg.tone, "magic");
|
||||
// The individual/consumer surface now sends "Magic" (capitalized), matching
|
||||
// the enterprise tone literal — the #10718 lowercase "magic" is stale.
|
||||
assert.equal(arg.tone, "Magic");
|
||||
assert.equal(arg.isSbsSupported, true);
|
||||
assert.equal(arg.renderReferencesBehindEOS, true);
|
||||
assert.deepEqual(arg.extraExtensionParameters, {});
|
||||
assert.deepEqual(arg.plugins, [{ Id: "BingWebSearch", Source: "BuiltIn" }]);
|
||||
// Sent on every tier now, not gated to enterprise as #8971 described.
|
||||
assert.equal(arg.disconnectBehavior, "continue");
|
||||
});
|
||||
|
||||
test("#10718: the message object carries the recaptured rich shape", () => {
|
||||
test("2026-08-21: the message object carries the recaptured rich shape", () => {
|
||||
const arg = buildChatInvocation({
|
||||
text: "Say OK in one word.",
|
||||
traceId: "t",
|
||||
@@ -93,7 +118,9 @@ test("#10718: the message object carries the recaptured rich shape", () => {
|
||||
"adaptiveCards",
|
||||
"attachments",
|
||||
"author",
|
||||
"clientInfo",
|
||||
"clientPreferences",
|
||||
"connectedFederatedConnections",
|
||||
"entityAnnotationTypes",
|
||||
"experienceType",
|
||||
"inputMethod",
|
||||
@@ -107,26 +134,41 @@ test("#10718: the message object carries the recaptured rich shape", () => {
|
||||
assert.equal(message.messageType, "Chat");
|
||||
assert.equal(message.requestId, "r");
|
||||
assert.equal(message.experienceType, "Default");
|
||||
assert.deepEqual(message.entityAnnotationTypes, ["People", "File", "Event", "Email", "TeamsMessage"]);
|
||||
assert.deepEqual(message.entityAnnotationTypes, [
|
||||
"People",
|
||||
"File",
|
||||
"Event",
|
||||
"Email",
|
||||
"TeamsMessage",
|
||||
]);
|
||||
assert.equal(message.attachments, null);
|
||||
assert.deepEqual(message.locationInfo, { timeZone: "UTC", timeZoneOffset: 0 });
|
||||
assert.deepEqual(message.connectedFederatedConnections, ["dummyId"]);
|
||||
// Same clientInfo object echoed inside message, per the capture.
|
||||
assert.deepEqual(message.clientInfo, arg.clientInfo);
|
||||
});
|
||||
|
||||
test("#10718: default tier lists are the recaptured 14-entry optionsSets / 6-entry allowedMessageTypes", () => {
|
||||
test("2026-08-21: default tier lists are the recaptured 34-entry optionsSets / 30-entry allowedMessageTypes", () => {
|
||||
const overrides = resolveChatInvocationOverrides(undefined);
|
||||
assert.equal(overrides.optionsSets.length, 14);
|
||||
assert.equal(overrides.allowedMessageTypes.length, 6);
|
||||
assert.equal(overrides.tone, "magic");
|
||||
// The pre-#10718 consumer/MSA flags are gone from the wire.
|
||||
assert.equal(overrides.optionsSets.length, 34);
|
||||
assert.equal(overrides.allowedMessageTypes.length, 30);
|
||||
assert.equal(overrides.tone, "Magic");
|
||||
assert.equal(overrides.disconnectBehavior, "continue");
|
||||
const optionSets = M365_DEFAULT_OPTION_SETS as readonly string[];
|
||||
const messageTypes = ALLOWED_MESSAGE_TYPES as readonly string[];
|
||||
for (const stale of ["enable_msa_user", "pdnascan", "cwc_code_interpreter", "rich_responses"]) {
|
||||
assert.ok(!optionSets.includes(stale), `${stale} must not be in the default option sets`);
|
||||
// Entries the 2026-08-21 capture showed that the #10718 lists lacked.
|
||||
for (const present of [
|
||||
"cwc_code_interpreter",
|
||||
"rich_responses",
|
||||
"async_client_interaction",
|
||||
"flux_v3_references",
|
||||
]) {
|
||||
assert.ok(optionSets.includes(present), `${present} must be in the default option sets`);
|
||||
}
|
||||
for (const stale of ["InternalSearchQuery", "GeneratedCode", "RenderCardRequest", "AdsQuery", "SemanticSerp", "GenerateContentQuery"]) {
|
||||
assert.ok(!messageTypes.includes(stale), `${stale} must not be in allowedMessageTypes`);
|
||||
for (const present of ["InternalSearchQuery", "GeneratedCode", "AuthError", "TriggerPlugin"]) {
|
||||
assert.ok(messageTypes.includes(present), `${present} must be in allowedMessageTypes`);
|
||||
}
|
||||
// Entries the capture showed and the old lists lacked.
|
||||
// Entries the #10718 capture showed and this capture still confirms.
|
||||
assert.ok(optionSets.includes("cwcfluxgptv"));
|
||||
assert.ok(messageTypes.includes("EndOfRequest"));
|
||||
});
|
||||
@@ -134,8 +176,7 @@ test("#10718: default tier lists are the recaptured 14-entry optionsSets / 6-ent
|
||||
// ── refresh_token helpers ──────────────────────────────────────────────────
|
||||
|
||||
function fakeJwt(claims: Record<string, unknown>): string {
|
||||
const b64 = (value: unknown) =>
|
||||
Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||
const b64 = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||
return `${b64({ alg: "none" })}.${b64(claims)}.sig`;
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,9 @@ test("buildChatInvocation defaults stay backward compatible", () => {
|
||||
requestId: "r",
|
||||
conversationId: "c",
|
||||
}).arguments[0] as Record<string, unknown>;
|
||||
assert.deepEqual(arg.plugins, []);
|
||||
// 2026-08-21 capture (#11069): BingWebSearch BuiltIn plugin is now the
|
||||
// default on all tiers (was [] in the #10718 shape); toolChoice stays null.
|
||||
assert.deepEqual(arg.plugins, [{ Id: "BingWebSearch", Source: "BuiltIn" }]);
|
||||
assert.equal(arg.toolChoice, null);
|
||||
assert.equal(arg.customInstructions, undefined);
|
||||
});
|
||||
|
||||
78
tests/unit/dashboard/m365-har-import.test.ts
Normal file
78
tests/unit/dashboard/m365-har-import.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
extractM365CredentialFromHar,
|
||||
describeHarImportExpiry,
|
||||
M365_CHATHUB_WS_PREFIX,
|
||||
} from "../../../src/shared/utils/m365HarImport.ts";
|
||||
|
||||
function fakeJwt(exp: number): string {
|
||||
const b64 = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||
return `${b64({ alg: "none" })}.${b64({ exp })}.sig`;
|
||||
}
|
||||
|
||||
function harWithUrl(url: string | null): string {
|
||||
return JSON.stringify({
|
||||
log: { entries: url ? [{ request: { url } }] : [] },
|
||||
});
|
||||
}
|
||||
|
||||
test("extracts access_token + chathubPath from a matching ChatHub WS entry", () => {
|
||||
const exp = Math.floor(Date.now() / 1000) + 3600;
|
||||
const token = fakeJwt(exp);
|
||||
const url = `${M365_CHATHUB_WS_PREFIX}oid-123%40tenant-456?chatsessionid=abc&access_token=${token}`;
|
||||
const result = extractM365CredentialFromHar(harWithUrl(url));
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
assert.equal(result.chathubPath, "oid-123@tenant-456");
|
||||
assert.equal(result.apiKey, `access_token=${token}; chathubPath=oid-123@tenant-456`);
|
||||
assert.ok(result.expiresAt !== null && Math.abs(result.expiresAt - exp * 1000) < 1000);
|
||||
});
|
||||
|
||||
test("picks the LAST matching entry when a HAR has multiple turns", () => {
|
||||
const oldToken = fakeJwt(1000);
|
||||
const freshToken = fakeJwt(9999999999);
|
||||
const har = JSON.stringify({
|
||||
log: {
|
||||
entries: [
|
||||
{ request: { url: `${M365_CHATHUB_WS_PREFIX}u%40t?access_token=${oldToken}` } },
|
||||
{ request: { url: "https://unrelated.example/" } },
|
||||
{ request: { url: `${M365_CHATHUB_WS_PREFIX}u%40t?access_token=${freshToken}` } },
|
||||
],
|
||||
},
|
||||
});
|
||||
const result = extractM365CredentialFromHar(har);
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
assert.ok(result.apiKey.includes(freshToken));
|
||||
});
|
||||
|
||||
test("returns notJson for malformed input", () => {
|
||||
const result = extractM365CredentialFromHar("not json{{{");
|
||||
assert.deepEqual(result, { ok: false, error: "notJson" });
|
||||
});
|
||||
|
||||
test("returns noEntries when log.entries is missing/not an array", () => {
|
||||
const result = extractM365CredentialFromHar(JSON.stringify({ log: {} }));
|
||||
assert.deepEqual(result, { ok: false, error: "noEntries" });
|
||||
});
|
||||
|
||||
test("returns noChathubUrl when no request matches the ChatHub prefix", () => {
|
||||
const result = extractM365CredentialFromHar(harWithUrl("https://example.com/"));
|
||||
assert.deepEqual(result, { ok: false, error: "noChathubUrl" });
|
||||
});
|
||||
|
||||
test("returns missingFields when the URL matches but lacks access_token or chathubPath", () => {
|
||||
const result = extractM365CredentialFromHar(harWithUrl(`${M365_CHATHUB_WS_PREFIX}`));
|
||||
assert.equal(result.ok, false);
|
||||
});
|
||||
|
||||
test("describeHarImportExpiry classifies ok/warn/bad/unknown", () => {
|
||||
const now = Date.now();
|
||||
assert.equal(describeHarImportExpiry(now + 30 * 60000, now).tone, "ok");
|
||||
assert.equal(describeHarImportExpiry(now + 5 * 60000, now).tone, "warn");
|
||||
assert.equal(describeHarImportExpiry(now - 60000, now).tone, "bad");
|
||||
assert.equal(describeHarImportExpiry(null, now).tone, "unknown");
|
||||
});
|
||||
@@ -14,11 +14,15 @@ describe("CommandCodeExecutor", () => {
|
||||
assert.ok(executor);
|
||||
});
|
||||
|
||||
it("buildUrl returns a string", () => {
|
||||
it("buildUrl targets the documented /provider/v1/chat/completions endpoint (#10265)", () => {
|
||||
const executor = new mod.CommandCodeExecutor();
|
||||
const url = executor.buildUrl();
|
||||
assert.ok(typeof url === "string");
|
||||
assert.ok(url.includes("generate") && url.includes("commandcode"));
|
||||
assert.ok(
|
||||
url.includes("/provider/v1/chat/completions"),
|
||||
`expected the documented provider API endpoint, got: ${url}`
|
||||
);
|
||||
assert.ok(url.includes("commandcode"));
|
||||
});
|
||||
|
||||
it("execute throws when no API key", async () => {
|
||||
@@ -57,7 +61,7 @@ describe("CommandCodeExecutor", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("assistant tool-call conversion always emits a valid required arguments field (#regression input[N] missing required field arguments)", async () => {
|
||||
it("posts a flat OpenAI chat.completions body (no CLI envelope) to /provider/v1/chat/completions (#10265)", async () => {
|
||||
const calls: Array<{ url: string; init: RequestInit; body: unknown }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
@@ -70,182 +74,8 @@ describe("CommandCodeExecutor", () => {
|
||||
}) as typeof fetch;
|
||||
|
||||
const executor = new mod.CommandCodeExecutor();
|
||||
const pairedId = "call_paired";
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "user", content: "hi" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: "",
|
||||
tool_calls: [
|
||||
// Missing arguments entirely -> must still get a valid arguments field
|
||||
{ id: "call_missing", type: "function", function: { name: "lookup" } },
|
||||
// Empty string arguments -> "{}"
|
||||
{
|
||||
id: "call_empty",
|
||||
type: "function",
|
||||
function: { name: "lookup", arguments: "" },
|
||||
},
|
||||
// Valid object arguments -> round-trips as JSON string
|
||||
{
|
||||
id: pairedId,
|
||||
type: "function",
|
||||
function: { name: "lookup", arguments: { q: "docs" } },
|
||||
},
|
||||
// Valid string arguments -> preserved as-is
|
||||
{
|
||||
id: "call_string",
|
||||
type: "function",
|
||||
function: { name: "lookup", arguments: '{"q":"string"}' },
|
||||
},
|
||||
// Invalid JSON string arguments -> defaults to "{}"
|
||||
{
|
||||
id: "call_invalid",
|
||||
type: "function",
|
||||
function: { name: "lookup", arguments: "{invalid-json" },
|
||||
},
|
||||
// Tool call without name -> defaults tool-result toolName to "unknown"
|
||||
{
|
||||
id: "call_unnamed",
|
||||
type: "function",
|
||||
function: { arguments: { q: "unnamed" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "tool", tool_call_id: "call_missing", content: "r1" },
|
||||
{ role: "tool", tool_call_id: "call_empty", content: "r2" },
|
||||
{ role: "tool", tool_call_id: pairedId, content: "r3" },
|
||||
{ role: "tool", tool_call_id: "call_string", content: "r4" },
|
||||
{ role: "tool", tool_call_id: "call_invalid", content: "r5" },
|
||||
{ role: "tool", tool_call_id: "call_unnamed", content: "r6" },
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
await executor.execute({
|
||||
model: "test",
|
||||
body,
|
||||
stream: false,
|
||||
credentials: { apiKey: "fake-key" },
|
||||
signal: null,
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.equal(calls.length, 1, "exactly one upstream call");
|
||||
const sentBody = calls[0].body as {
|
||||
params: { messages: Array<{ role: string; content: unknown }> };
|
||||
};
|
||||
const assistant = sentBody.params.messages.find((m) => m.role === "assistant");
|
||||
assert.ok(assistant, "assistant turn present");
|
||||
const parts = assistant.content as Array<Record<string, unknown>>;
|
||||
const toolCalls = parts.filter((p) => p.type === "tool-call");
|
||||
assert.equal(toolCalls.length, 6, "all six paired tool calls converted");
|
||||
|
||||
for (const call of toolCalls) {
|
||||
assert.equal(
|
||||
typeof call.arguments,
|
||||
"string",
|
||||
`tool-call ${String(call.toolCallId)} must carry a string arguments field`
|
||||
);
|
||||
const parsed = JSON.parse(call.arguments as string);
|
||||
assert.equal(typeof parsed, "object");
|
||||
assert.ok(!Array.isArray(parsed), "arguments must parse to a JSON object");
|
||||
}
|
||||
|
||||
const byId = new Map(toolCalls.map((c) => [String(c.toolCallId), c]));
|
||||
assert.equal(byId.get("call_missing").arguments, "{}", "missing arguments -> empty object");
|
||||
assert.equal(byId.get("call_empty").arguments, "{}", "empty string arguments -> empty object");
|
||||
assert.equal(
|
||||
byId.get(pairedId).arguments,
|
||||
'{"q":"docs"}',
|
||||
"object arguments round-trip as JSON string"
|
||||
);
|
||||
assert.equal(
|
||||
byId.get("call_string").arguments,
|
||||
'{"q":"string"}',
|
||||
"valid string arguments preserved as-is"
|
||||
);
|
||||
assert.equal(
|
||||
byId.get("call_invalid").arguments,
|
||||
"{}",
|
||||
"invalid JSON string arguments -> empty object"
|
||||
);
|
||||
|
||||
const toolMsgs = sentBody.params.messages.filter((m) => m.role === "tool");
|
||||
assert.equal(toolMsgs.length, 6, "all 6 tool result messages present");
|
||||
const resultByName = new Map(
|
||||
toolMsgs.map((m) => {
|
||||
const p = (m.content as Array<Record<string, unknown>>)[0];
|
||||
return [String(p.toolCallId), String(p.toolName)];
|
||||
})
|
||||
);
|
||||
assert.equal(resultByName.get("call_missing"), "lookup");
|
||||
assert.equal(
|
||||
resultByName.get("call_unnamed"),
|
||||
"unknown",
|
||||
"unnamed call falls back to 'unknown'"
|
||||
);
|
||||
|
||||
// /alpha/generate also requires `arguments` on tool-result parts; a
|
||||
// missing one is rejected with `input[N] missing required field 'arguments'`
|
||||
// (the index landing on the tool message). Echo the paired call's
|
||||
// normalized arguments.
|
||||
const resultById = new Map(
|
||||
toolMsgs.map((m) => {
|
||||
const p = (m.content as Array<Record<string, unknown>>)[0];
|
||||
return [String(p.toolCallId), p];
|
||||
})
|
||||
);
|
||||
assert.equal(resultById.size, 6, "each tool result maps to its call id");
|
||||
for (const p of resultById.values()) {
|
||||
assert.equal(
|
||||
typeof p.arguments,
|
||||
"string",
|
||||
`tool-result ${String(p.toolCallId)} must carry a string arguments field`
|
||||
);
|
||||
const parsed = JSON.parse(p.arguments as string);
|
||||
assert.equal(typeof parsed, "object");
|
||||
assert.ok(!Array.isArray(parsed), "tool-result arguments must parse to a JSON object");
|
||||
}
|
||||
assert.equal(
|
||||
resultById.get("call_missing").arguments,
|
||||
"{}",
|
||||
"tool-result echoes paired call's missing arguments as empty object"
|
||||
);
|
||||
assert.equal(
|
||||
resultById.get(pairedId).arguments,
|
||||
'{"q":"docs"}',
|
||||
"tool-result echoes paired call's object arguments as JSON string"
|
||||
);
|
||||
assert.equal(
|
||||
resultById.get("call_string").arguments,
|
||||
'{"q":"string"}',
|
||||
"tool-result echoes paired call's valid string arguments as-is"
|
||||
);
|
||||
assert.equal(
|
||||
resultById.get("call_empty").arguments,
|
||||
"{}",
|
||||
"tool-result echoes paired call's empty arguments as empty object"
|
||||
);
|
||||
assert.equal(
|
||||
resultById.get("call_invalid").arguments,
|
||||
"{}",
|
||||
"tool-result echoes paired call's invalid JSON arguments as empty object"
|
||||
);
|
||||
});
|
||||
|
||||
it("COMMAND_CODE_VERSION default constant is 1.15.1", () => {
|
||||
assert.equal(mod.COMMAND_CODE_VERSION, "1.15.1");
|
||||
});
|
||||
|
||||
it("renames tool names colliding with upstream built-ins on the wire and un-renames on the response (#regression input[N] missing required field arguments from a tool_search result)", async () => {
|
||||
// Upstream /alpha/generate normalizes tool-call/result parts against its
|
||||
// OWN built-in registry for matching names; `tool_search` collides and its
|
||||
// result is rejected with `input[N] missing required field 'arguments'`.
|
||||
// Verified live: renaming the pair to a non-colliding name passes.
|
||||
const body = {
|
||||
model: "gpt-5.4",
|
||||
messages: [
|
||||
{ role: "user", content: "hi" },
|
||||
{
|
||||
@@ -253,29 +83,19 @@ describe("CommandCodeExecutor", () => {
|
||||
content: "",
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_00_AAAAAAAAAAAAAAAAA",
|
||||
id: "call_1",
|
||||
type: "function",
|
||||
function: { name: "tool_search", arguments: '{"query":"x"}' },
|
||||
},
|
||||
{
|
||||
id: "call_01_BBBBBBBBBBBBBBBBB",
|
||||
type: "function",
|
||||
function: { name: "lookup", arguments: '{"q":"1"}' },
|
||||
function: { name: "lookup", arguments: '{"q":"docs"}' },
|
||||
},
|
||||
// Missing arguments entirely stays missing — passthrough, no CLI
|
||||
// envelope injection of a synthetic `arguments` field.
|
||||
{ id: "call_2", type: "function", function: { name: "search" } },
|
||||
],
|
||||
},
|
||||
{ role: "tool", tool_call_id: "call_00_AAAAAAAAAAAAAAAAA", content: "r1" },
|
||||
{ role: "tool", tool_call_id: "call_01_BBBBBBBBBBBBBBBBB", content: "r2" },
|
||||
{ role: "tool", tool_call_id: "call_1", content: "r1" },
|
||||
{ role: "tool", tool_call_id: "call_2", content: "r2" },
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "tool_search",
|
||||
description: "search tools",
|
||||
parameters: { type: "object", properties: { query: { type: "string" } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -287,31 +107,9 @@ describe("CommandCodeExecutor", () => {
|
||||
],
|
||||
};
|
||||
|
||||
const calls: Array<{ url: string; init: RequestInit; body: unknown }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
// execute() makes a single upstream fetch; capture the wire request and
|
||||
// return a stream with a tool-call event using the renamed wire name.
|
||||
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
calls.push({
|
||||
url: String(url),
|
||||
init: init || {},
|
||||
body: JSON.parse(String((init as RequestInit | undefined)?.body)),
|
||||
});
|
||||
const streamBody =
|
||||
'data: {"type":"tool-call","toolCallId":"c1","toolName":"omniroute_tool_search","input":{"query":"x"}}\n\n' +
|
||||
'data: {"type":"finish","finishReason":"tool_use"}\n\n' +
|
||||
"data: [DONE]\n\n";
|
||||
return new Response(streamBody, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
const executor = new mod.CommandCodeExecutor();
|
||||
let result: { response: Response } | null = null;
|
||||
try {
|
||||
result = await executor.execute({
|
||||
model: "test",
|
||||
await executor.execute({
|
||||
model: "gpt-5.4",
|
||||
body,
|
||||
stream: false,
|
||||
credentials: { apiKey: "fake-key" },
|
||||
@@ -321,46 +119,85 @@ describe("CommandCodeExecutor", () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
const sentBody = calls[0].body as {
|
||||
params: {
|
||||
messages: Array<{ role: string; content: unknown }>;
|
||||
tools: Array<{ name: string }>;
|
||||
};
|
||||
};
|
||||
const toolDefNames = sentBody.params.tools.map((t) => t.name);
|
||||
assert.equal(calls.length, 1, "exactly one upstream call");
|
||||
assert.ok(
|
||||
toolDefNames.includes("omniroute_tool_search"),
|
||||
"colliding tool def renamed on the wire"
|
||||
calls[0].url.includes("/provider/v1/chat/completions"),
|
||||
`expected documented provider endpoint, got: ${calls[0].url}`
|
||||
);
|
||||
assert.ok(toolDefNames.includes("lookup"), "non-colliding tool def untouched");
|
||||
|
||||
const assistant = sentBody.params.messages.find((m) => m.role === "assistant");
|
||||
const toolCallParts = (assistant?.content as Array<Record<string, unknown>>).filter(
|
||||
(p) => p.type === "tool-call"
|
||||
const sent = calls[0].body as Record<string, unknown>;
|
||||
// No CLI envelope.
|
||||
assert.equal(sent.config, undefined, "CLI envelope `config` must not be sent");
|
||||
assert.equal(sent.params, undefined, "CLI envelope `params` wrapper must not be sent");
|
||||
assert.equal(sent.model, "gpt-5.4", "flat OpenAI model at top level");
|
||||
assert.equal((sent.messages as Array<{ role: string }>)[0].role, "user");
|
||||
// Assistant tool_calls pass through unchanged (no CLI tool-call/tool-result parts).
|
||||
const assistant = (sent.messages as Array<Record<string, unknown>>).find(
|
||||
(m) => m.role === "assistant"
|
||||
);
|
||||
const toolSearchCall = toolCallParts.find((p) => p.toolName === "omniroute_tool_search");
|
||||
assert.ok(toolSearchCall, "assistant tool-call part renamed on the wire");
|
||||
const lookupCall = toolCallParts.find((p) => p.toolName === "lookup");
|
||||
assert.ok(lookupCall, "non-colliding tool-call part untouched");
|
||||
|
||||
const toolMsgs = sentBody.params.messages.filter((m) => m.role === "tool");
|
||||
const toolSearchResult = toolMsgs.find(
|
||||
(m) => (m.content as Array<Record<string, unknown>>)[0]?.toolName === "omniroute_tool_search"
|
||||
assert.ok(assistant, "assistant turn present");
|
||||
const toolCalls = assistant?.tool_calls as Array<{
|
||||
id: string;
|
||||
function: { name: string; arguments?: string };
|
||||
}>;
|
||||
assert.equal(toolCalls.length, 2, "both tool calls pass through untouched");
|
||||
assert.equal(toolCalls[0].function.name, "lookup");
|
||||
assert.equal(toolCalls[0].function.arguments, '{"q":"docs"}');
|
||||
assert.equal(toolCalls[1].function.arguments, undefined, "missing arguments stays missing (no injection)");
|
||||
// Tool role message (OpenAI flat) preserved.
|
||||
const toolMsg = (sent.messages as Array<Record<string, unknown>>).find(
|
||||
(m) => m.role === "tool"
|
||||
);
|
||||
assert.ok(toolSearchResult, "tool-result part renamed on the wire");
|
||||
|
||||
// Response path: upstream emits the renamed wire name; the client must get
|
||||
// its original name back.
|
||||
assert.ok(result, "execute returned a response");
|
||||
const json = (await result.response.json()) as {
|
||||
choices: Array<{ message: { tool_calls?: Array<{ function: { name: string } }> } }>;
|
||||
};
|
||||
const toolCalls = json.choices[0].message.tool_calls ?? [];
|
||||
assert.equal(toolCalls.length, 1, "one tool call translated");
|
||||
assert.equal(toolMsg?.tool_call_id, "call_1");
|
||||
assert.equal(
|
||||
toolCalls[0].function.name,
|
||||
"tool_search",
|
||||
"renamed wire name un-renamed for the client"
|
||||
(sent.tools as Array<{ function: { name: string } }>)[0].function.name,
|
||||
"lookup",
|
||||
"tool definitions pass through in OpenAI shape (no rename)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("passes through the upstream OpenAI response and drops CLI-impersonation headers (#10265)", async () => {
|
||||
const calls: Array<{ url: string; init: RequestInit }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
calls.push({ url: String(url), init: init || {} });
|
||||
const chunk =
|
||||
'data: {"id":"c1","object":"chat.completion.chunk","model":"gpt-5.4",' +
|
||||
'"choices":[{"index":0,"delta":{"content":"hi"}}]}\n\n' +
|
||||
'data: {"id":"c1","object":"chat.completion.chunk","model":"gpt-5.4",' +
|
||||
'"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":' +
|
||||
'{"prompt_tokens":2,"completion_tokens":1,"total_tokens":3}}\n\n' +
|
||||
"data: [DONE]\n\n";
|
||||
return new Response(chunk, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
const executor = new mod.CommandCodeExecutor();
|
||||
let result: { response: Response; headers: Record<string, string> } | null = null;
|
||||
try {
|
||||
result = await executor.execute({
|
||||
model: "gpt-5.4",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: { apiKey: "fake-key" },
|
||||
signal: null,
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.ok(result, "execute returned a result");
|
||||
const headers = result.headers;
|
||||
assert.equal(headers["x-command-code-version"], undefined, "CLI-impersonation header dropped");
|
||||
assert.equal(headers["x-cli-environment"], undefined, "CLI-impersonation header dropped");
|
||||
assert.equal(headers.Authorization, "Bearer fake-key");
|
||||
|
||||
// The upstream OpenAI SSE passes through untouched (no CLI re-parsing).
|
||||
const text = await result.response.text();
|
||||
assert.ok(text.includes("chat.completion.chunk"), "OpenAI-format SSE passed through");
|
||||
assert.ok(text.includes('"content":"hi"'), "delta content preserved");
|
||||
assert.ok(text.includes("[DONE]"), "stream terminator preserved");
|
||||
assert.ok(text.includes('"prompt_tokens":2'), "OpenAI usage block passed through");
|
||||
});
|
||||
});
|
||||
@@ -131,7 +131,22 @@ test("buildChatInvocation produces a type:4 chat invocation carrying the user te
|
||||
assert.equal(arg.sessionId, "session-id");
|
||||
assert.equal(arg.conversationId, "conversation-id");
|
||||
assert.equal(arg.productThreadType, "Office");
|
||||
assert.deepEqual(arg.clientInfo, { clientAppName: "Office", clientPlatform: "mcmcopilot-web" });
|
||||
// 2026-08-21 capture (#11069): clientInfo gained 8 keys (clientEntrypoint,
|
||||
// clientSessionId, ProductCategory, clientAppType, productEntryPoint,
|
||||
// deviceOS, deviceType, clientPlatformVersion). The #10718 base shape only
|
||||
// carried clientAppName + clientPlatform.
|
||||
assert.deepEqual(arg.clientInfo, {
|
||||
clientAppName: "Office",
|
||||
clientPlatform: "mcmcopilot-web",
|
||||
clientEntrypoint: "mcmcopilot-officeweb",
|
||||
clientSessionId: "session-id",
|
||||
ProductCategory: "Chat",
|
||||
clientAppType: "Web",
|
||||
productEntryPoint: "ChatPanel",
|
||||
deviceOS: "Windows",
|
||||
deviceType: "Desktop",
|
||||
clientPlatformVersion: "10",
|
||||
});
|
||||
assert.equal(arg.isStartOfSession, true);
|
||||
assert.ok(Array.isArray(arg.optionsSets));
|
||||
assert.ok((arg.optionsSets as string[]).includes("enable_gg_gpt"));
|
||||
|
||||
@@ -30,10 +30,11 @@ test("model-driven tone overrides the tier default; bare id keeps the tier tone"
|
||||
const enterprise = resolveChatInvocationOverrides("enterprise");
|
||||
const individual = resolveChatInvocationOverrides(undefined);
|
||||
|
||||
// enterprise tier default tone is Magic; individual/EDU sends "magic" (#10718
|
||||
// recapture — the old "" default was part of the silently-dropped shape)
|
||||
// enterprise tier default tone is Magic; individual/EDU now also sends
|
||||
// "Magic" (capitalized) — the 2026-08-21 capture (#11069) showed lowercase
|
||||
// "magic" is part of the shape that gets silently dropped (ping-only socket).
|
||||
assert.equal(enterprise.tone, "Magic");
|
||||
assert.equal(individual.tone, "magic");
|
||||
assert.equal(individual.tone, "Magic");
|
||||
|
||||
// precedence: resolveToneForModel(model) ?? overrides.tone (mirrors the executor wiring)
|
||||
const toneFor = (model: string | undefined, tierTone: string) =>
|
||||
@@ -43,9 +44,9 @@ test("model-driven tone overrides the tier default; bare id keeps the tier tone"
|
||||
assert.equal(toneFor("copilot-m365-claude-opus", enterprise.tone), "Claude_Opus");
|
||||
assert.equal(toneFor("copilot-m365-claude-opus", individual.tone), "Claude_Opus");
|
||||
|
||||
// the bare id keeps whatever the tier resolved
|
||||
// the bare id keeps whatever the tier resolved (both "Magic" post-#11069)
|
||||
assert.equal(toneFor("copilot-m365", enterprise.tone), "Magic");
|
||||
assert.equal(toneFor("copilot-m365", individual.tone), "magic");
|
||||
assert.equal(toneFor("copilot-m365", individual.tone), "Magic");
|
||||
});
|
||||
|
||||
test("registry exposes the bare id (first) plus every tone variant", () => {
|
||||
|
||||
@@ -8,12 +8,12 @@ import { resolveChatCoreTargetFormat } from "../../open-sse/handlers/chatCore/ta
|
||||
// ghe-copilot catalog. getModelTargetFormat falls back to getGlobalModel() when
|
||||
// the provider's own catalog lacks the model id, importing the DECLARING
|
||||
// provider's endpoint semantics into every other provider serving the same id.
|
||||
// command-code's chat-shaped /alpha/generate executor then received a
|
||||
// command-code's chat-shaped executor then received a
|
||||
// Responses-format body (input, not messages) and shipped `messages: []`
|
||||
// upstream — upstream rejected with "Invalid prompt: messages must not be empty"
|
||||
// (502). Model-level targetFormat is provider-scoped: it must not leak.
|
||||
test("model-level targetFormat does not leak across provider catalogs", () => {
|
||||
// command-code serves gpt-5.6-luna over its chat-shaped /alpha/generate endpoint
|
||||
// command-code serves gpt-5.6-luna over its chat-shaped provider endpoint
|
||||
assert.equal(getModelTargetFormat("cmd", "gpt-5.6-luna"), null);
|
||||
// raw provider id form behaves identically (alias resolution)
|
||||
assert.equal(getModelTargetFormat("command-code", "gpt-5.6-luna"), null);
|
||||
|
||||
@@ -16,7 +16,6 @@ const { __setTlsFetchOverrideForTesting: __setPplxTlsFetchOverride } =
|
||||
const { __setTlsFetchOverrideForTesting: __setGrokTlsFetchOverride } =
|
||||
await import("../../open-sse/services/grokTlsClient.ts");
|
||||
|
||||
const { COMMAND_CODE_VERSION } = await import("../../open-sse/executors/commandCode.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@@ -216,11 +215,11 @@ test("specialty provider validators cover Deepgram, AssemblyAI, ElevenLabs and I
|
||||
|
||||
test("validateCommandCodeProvider ignores caller baseUrl and chatPath overrides", async () => {
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
assert.equal(String(url), "https://api.commandcode.ai/alpha/generate");
|
||||
assert.equal(String(url), "https://api.commandcode.ai/provider/v1/chat/completions");
|
||||
const headers = init.headers as Record<string, string>;
|
||||
assert.equal(headers.Authorization, "Bearer cc-key");
|
||||
const body = JSON.parse(String(init.body));
|
||||
assert.equal(body.params.model, "command-code-validation-model");
|
||||
assert.equal(body.model, "command-code-validation-model");
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
||||
};
|
||||
|
||||
@@ -239,7 +238,7 @@ test("validateCommandCodeProvider ignores caller baseUrl and chatPath overrides"
|
||||
test("validateCommandCodeProvider defaults probe model to DeepSeek flash", async () => {
|
||||
globalThis.fetch = async (_url, init = {}) => {
|
||||
const body = JSON.parse(String(init.body));
|
||||
assert.equal(body.params.model, "deepseek/deepseek-v4-flash");
|
||||
assert.equal(body.model, "deepseek/deepseek-v4-flash");
|
||||
return new Response("", { status: 400 });
|
||||
};
|
||||
|
||||
@@ -2285,7 +2284,7 @@ test("specialty validator rejects invalid Runway credentials", async () => {
|
||||
assert.equal(runway.error, "Invalid API key");
|
||||
});
|
||||
|
||||
test("validateCommandCodeProvider sends Command Code probe URL, headers, and wrapper body", async () => {
|
||||
test("validateCommandCodeProvider sends Command Code probe URL, headers, and flat OpenAI body", async () => {
|
||||
const calls: Array<{
|
||||
url: string;
|
||||
method?: string;
|
||||
@@ -2309,22 +2308,21 @@ test("validateCommandCodeProvider sends Command Code probe URL, headers, and wra
|
||||
|
||||
assert.deepEqual(result, { valid: true, error: null });
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].url, "https://api.commandcode.ai/alpha/generate");
|
||||
// Probe targets the documented /provider/v1/chat/completions endpoint, not
|
||||
// the CLI-only /alpha/generate (#10265).
|
||||
assert.equal(calls[0].url, "https://api.commandcode.ai/provider/v1/chat/completions");
|
||||
assert.equal(calls[0].method, "POST");
|
||||
assert.equal(calls[0].headers.Authorization, "Bearer cc_test_key");
|
||||
assert.equal(calls[0].headers["Content-Type"], "application/json");
|
||||
assert.equal(calls[0].headers["x-command-code-version"], COMMAND_CODE_VERSION);
|
||||
assert.equal(calls[0].headers["x-cli-environment"], "external");
|
||||
assert.equal(calls[0].headers["x-project-slug"], "pi-cc");
|
||||
assert.equal(calls[0].headers["x-taste-learning"], "false");
|
||||
assert.equal(calls[0].headers["x-co-flag"], "false");
|
||||
assert.equal(typeof calls[0].headers["x-session-id"], "string");
|
||||
assert.equal(calls[0].body.config.environment, "external");
|
||||
assert.equal(calls[0].body.permissionMode, "standard");
|
||||
assert.equal(calls[0].body.skills, "");
|
||||
assert.equal(calls[0].body.params.model, "gpt-5.4-mini");
|
||||
assert.equal(calls[0].body.params.stream, true);
|
||||
assert.equal(calls[0].body.params.max_tokens, 1);
|
||||
// No CLI-impersonation headers.
|
||||
assert.equal(calls[0].headers["x-command-code-version"], undefined);
|
||||
assert.equal(calls[0].headers["x-cli-environment"], undefined);
|
||||
assert.equal(calls[0].headers["x-project-slug"], undefined);
|
||||
// Flat OpenAI chat.completions body (no CLI wrapper).
|
||||
assert.equal(calls[0].body.params, undefined, "CLI envelope params wrapper must not be sent");
|
||||
assert.equal(calls[0].body.model, "gpt-5.4-mini");
|
||||
assert.equal(calls[0].body.stream, true);
|
||||
assert.equal(calls[0].body.max_tokens, 1);
|
||||
});
|
||||
|
||||
for (const status of [400, 422, 429]) {
|
||||
|
||||
@@ -10,7 +10,6 @@ process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { handleResponsesCore } = await import("../../open-sse/handlers/responsesHandler.ts");
|
||||
const { COMMAND_CODE_VERSION } = await import("../../open-sse/executors/commandCode.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@@ -354,26 +353,31 @@ test("handleResponsesCore transforms Command Code executor SSE through Responses
|
||||
input: "hello command code",
|
||||
},
|
||||
responseFactory() {
|
||||
// /provider/v1/chat/completions returns standard OpenAI SSE (#10265).
|
||||
const chunk = (delta: Record<string, unknown>) =>
|
||||
`data: ${JSON.stringify({
|
||||
id: "c1",
|
||||
object: "chat.completion.chunk",
|
||||
model: "gpt-5.4-mini",
|
||||
choices: [{ index: 0, delta }],
|
||||
})}\n\n`;
|
||||
return new Response(
|
||||
[
|
||||
`data: ${JSON.stringify({ type: "text-delta", text: "command" })}`,
|
||||
"",
|
||||
`data: ${JSON.stringify({ type: "reasoning-delta", text: "thinking" })}`,
|
||||
"",
|
||||
`data: ${JSON.stringify({ type: "finish", finishReason: "stop" })}`,
|
||||
"",
|
||||
].join("\n"),
|
||||
{ status: 200, headers: { "Content-Type": "application/x-ndjson" } }
|
||||
chunk({ role: "assistant" }),
|
||||
chunk({ content: "command" }),
|
||||
chunk({}),
|
||||
].join("") + "data: [DONE]\n\n",
|
||||
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(call.url, "https://api.commandcode.ai/alpha/generate");
|
||||
assert.equal(call.url, "https://api.commandcode.ai/provider/v1/chat/completions");
|
||||
assert.equal(call.headers.Authorization, "Bearer cc_test_key");
|
||||
assert.equal(call.headers["x-command-code-version"], COMMAND_CODE_VERSION);
|
||||
assert.equal(call.body.params.model, "gpt-5.4-mini");
|
||||
assert.equal(call.body.params.stream, true);
|
||||
assert.equal(call.headers["x-command-code-version"], undefined);
|
||||
assert.equal(call.body.model, "gpt-5.4-mini");
|
||||
assert.equal(call.body.stream, true);
|
||||
|
||||
const sse = await result.response.text();
|
||||
assert.match(sse, /event: response\.created/);
|
||||
|
||||
@@ -40,6 +40,10 @@ db.prepare(
|
||||
`INSERT OR IGNORE INTO version_manager (tool, status, port, auto_start, auto_update, provider_expose)
|
||||
VALUES ('test-adopt', 'stopped', 29996, 0, 0, 0)`
|
||||
).run();
|
||||
db.prepare(
|
||||
`INSERT OR IGNORE INTO version_manager (tool, status, port, auto_start, auto_update, provider_expose)
|
||||
VALUES ('test-adopt-deny', 'stopped', 29994, 0, 0, 0)`
|
||||
).run();
|
||||
|
||||
const { ServiceSupervisor } = await import("../../../src/lib/services/ServiceSupervisor.ts");
|
||||
|
||||
@@ -213,6 +217,10 @@ test("does NOT auto-restart on crash", async () => {
|
||||
// the port, the supervisor ADOPTS it (marks running, no child spawned) instead
|
||||
// of spawning a duplicate that would die with EADDRINUSE.
|
||||
test("#6205: probeBeforeSpawn adopts a healthy existing instance (no spawn)", async () => {
|
||||
// GHSA-wg9p-6m2g-4v27: adoption of an already-healthy listener is opt-in
|
||||
// (a squatter can answer 2xx), so this adoption-path test opts in explicitly.
|
||||
const prevAdopt = process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE;
|
||||
process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE = "1";
|
||||
const healthServer = startHealthServer(29996);
|
||||
const cfg = { ...tickConfig("test-adopt", 29996), probeBeforeSpawn: true };
|
||||
const sup = new ServiceSupervisor(cfg);
|
||||
@@ -232,6 +240,8 @@ test("#6205: probeBeforeSpawn adopts a healthy existing instance (no spawn)", as
|
||||
} finally {
|
||||
await sup.stop();
|
||||
healthServer.close();
|
||||
if (prevAdopt === undefined) delete process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE;
|
||||
else process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE = prevAdopt;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -254,6 +264,9 @@ test("adopted service resolves and records the real pid of the process holding t
|
||||
// (#10523).
|
||||
const healthServer = startHealthServer(29995);
|
||||
const cfg = { ...tickConfig("test-adopt", 29995), probeBeforeSpawn: true };
|
||||
// Same opt-in as the adoption test above (GHSA-wg9p-6m2g-4v27).
|
||||
const prevAdopt = process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE;
|
||||
process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE = "1";
|
||||
const sup = new ServiceSupervisor(cfg);
|
||||
|
||||
try {
|
||||
@@ -268,5 +281,33 @@ test("adopted service resolves and records the real pid of the process holding t
|
||||
} finally {
|
||||
await sup.stop();
|
||||
healthServer.close();
|
||||
if (prevAdopt === undefined) delete process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE;
|
||||
else process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE = prevAdopt;
|
||||
}
|
||||
});
|
||||
|
||||
// GHSA-wg9p-6m2g-4v27: a healthy 2xx on the probed port no longer proves the
|
||||
// listener is this service — a local squatter can answer 200 and get adopted,
|
||||
// receiving the injected service API key. Without the operator opt-in the
|
||||
// supervisor must surface the actionable error instead of adopting.
|
||||
test("probeBeforeSpawn does NOT adopt a healthy listener without the opt-in", async () => {
|
||||
const healthServer = startHealthServer(29994);
|
||||
const cfg = { ...tickConfig("test-adopt-deny", 29994), probeBeforeSpawn: true };
|
||||
const prevAdopt = process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE;
|
||||
delete process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE;
|
||||
const sup = new ServiceSupervisor(cfg);
|
||||
|
||||
try {
|
||||
const status = await sup.start();
|
||||
assert.equal(status.state, "error", "a healthy listener is not adopted by default");
|
||||
assert.match(
|
||||
status.lastError ?? "",
|
||||
/OMNIROUTE_ADOPT_EXISTING_SERVICE/,
|
||||
"the error names the opt-in escape hatch"
|
||||
);
|
||||
} finally {
|
||||
await sup.stop();
|
||||
healthServer.close();
|
||||
if (prevAdopt !== undefined) process.env.OMNIROUTE_ADOPT_EXISTING_SERVICE = prevAdopt;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user