Compare commits

..

1 Commits

9 changed files with 51 additions and 108 deletions

View File

@@ -1 +0,0 @@
- fix(command-code): surface reasoning-only output as content when a model emits no text-delta (#10986)

View File

@@ -0,0 +1 @@
- fix(dashboard): treat UncloseAI as a no-auth provider so the connect form no longer forces a fake API key (#8864)

View File

@@ -455,10 +455,9 @@
"src/sse/handlers/chatHelpers.ts": 1019,
"src/shared/middleware/chatBodyAdmission.ts": 1005,
"_rebaseline_2026_08_20_10668_tabitoken_gateway": "#10668 (yawar-aquil) own catalog growth: src/shared/constants/providers/apikey/gateways.ts 1268->1283 (+15, entirely this PR diff -- one new tabitoken gateway entry, data lines only; base moved from 1255 to 1268 via other merges since the PR forked). Not combination drift: reproducible on the PR branch alone, so the WS5.5 release-captain rule does not apply. Extraction is not available -- the file is pure data (own header: \"Pure data; merged by apikey/index.ts via spread\") and already split into 6 family files under apikey/. Same precedent as _rebaseline_2026_08_14_imagetotext_servicekinds (#10275/#10291, gateways.ts 1250->1255, data lines only) and _rebaseline_2026_08_11_v3850_merge_storm_provider_registry (owner-authorized for this same file).",
"open-sse/executors/commandCode.ts": 1059,
"open-sse/executors/commandCode.ts": 1038,
"_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_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_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).",

View File

@@ -738,7 +738,6 @@ function createStreamResponse(
const decoder = new TextDecoder();
let buffer = "";
let sentRole = false;
let sentContent = false;
let closed = false;
const state: AggregateState = {
content: "",
@@ -773,10 +772,7 @@ function createStreamResponse(
switch (event.type) {
case "text-delta": {
const text = stringValue(event.text) || "";
if (text) {
sentContent = true;
controller.enqueue(sse(chatCompletionChunk(id, model, { content: text })));
}
if (text) controller.enqueue(sse(chatCompletionChunk(id, model, { content: text })));
state.content += text;
break;
}
@@ -814,12 +810,6 @@ function createStreamResponse(
break;
case "finish": {
state.finishReason = mapFinishReason(event.finishReason);
// If the model only produced reasoning-delta events (no text-delta), the
// client-visible stream would otherwise end with no content. Emit one
// content delta carrying the accumulated reasoning text (#10986).
if (!sentContent && state.reasoning && state.toolCalls.length === 0) {
controller.enqueue(sse(chatCompletionChunk(id, model, { content: state.reasoning })));
}
controller.enqueue(sse(chatCompletionChunk(id, model, {}, state.finishReason)));
// Emit a standards-compliant usage-only chunk (choices: []) before
// [DONE] when upstream reported usage. stream.ts's extractUsage
@@ -867,9 +857,6 @@ function createStreamResponse(
if (!closed) {
if (!sentRole)
controller.enqueue(sse(chatCompletionChunk(id, model, { role: "assistant" })));
if (!sentContent && state.reasoning && state.toolCalls.length === 0) {
controller.enqueue(sse(chatCompletionChunk(id, model, { content: state.reasoning })));
}
controller.enqueue(sse(chatCompletionChunk(id, model, {}, state.finishReason)));
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
@@ -960,14 +947,6 @@ async function createJsonResponse(
}
const message: JsonRecord = { role: "assistant", content: state.content };
// Some Command Code models emit the whole answer as reasoning-delta events and
// never a text-delta. When that leaves content empty, surface the reasoning text
// as content too (#10986) so OpenAI-compatible clients get a usable answer. Keep
// reasoning_content populated as well for reasoning-aware clients, and do not
// override content when real text OR tool calls are present.
if (!state.content && state.reasoning && state.toolCalls.length === 0) {
message.content = state.reasoning;
}
if (state.reasoning) message.reasoning_content = state.reasoning;
if (state.toolCalls.length > 0) message.tool_calls = state.toolCalls;

View File

@@ -639,20 +639,6 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
text: "Dahl auto-generates tokens via https://inference.dahl.global/tokens. No signup needed. Rate limits apply. You can also add your own API key.",
},
},
uncloseai: {
id: "uncloseai",
alias: "unc",
name: "UncloseAI",
icon: "auto_awesome",
color: "#8B5CF6",
textIcon: "UN",
website: "https://uncloseai.com",
hasFree: true,
freeNote: "Free forever — no signup, no credit card. OpenAI-compatible endpoints.",
passthroughModels: true,
authHint:
"No auth required. API accepts any non-empty string as key for identification. If older built-in models return 404, use Available Models → Import from /models or Auto-Sync; verified live model: solidrust/Hermes-3-Llama-3.1-8B-AWQ.",
},
hackclub: {
id: "hackclub",
alias: "hc",

View File

@@ -175,6 +175,25 @@ export const NOAUTH_PROVIDERS = {
text: "ZCode runs locally through its native app-server. OmniRoute never receives or stores the Z.ai credential.",
},
},
uncloseai: {
id: "uncloseai",
alias: "unc",
name: "UncloseAI",
icon: "auto_awesome",
color: "#8B5CF6",
textIcon: "UN",
website: "https://uncloseai.com",
noAuth: true,
hasFree: true,
passthroughModels: true,
serviceKinds: ["llm"],
authHint:
"No auth required. API accepts any non-empty string as key for identification. If older built-in models return 404, use Available Models → Import from /models or Auto-Sync; verified live model: solidrust/Hermes-3-Llama-3.1-8B-AWQ.",
freeNote: "Free forever — no signup, no credit card. OpenAI-compatible endpoints.",
notice: {
text: "UncloseAI needs no API key. API accepts any non-empty string as key for identification. If older built-in models return 404, use Available Models → Import from /models or Auto-Sync.",
},
},
aihorde: {
id: "aihorde",
alias: "horde",

View File

@@ -333,68 +333,6 @@ test("Command Code data: SSE lines aggregate into non-stream ChatCompletion JSON
});
});
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" });

View File

@@ -24,7 +24,8 @@
// (base-reds round 3, #9985) are both included in that measurement; Cursor API (specialty-media,
// #10729) brings it to 229; Token Kiosk (gateways, #10722) — merged in the same
// merge-train batch — independently bumped the gateways family too, landing at 231; Freebuff
// (gateways, #10531) brings it to 232.
// (gateways, #10531) brings it to 232. #8864 moves uncloseai (gateways family) into
// NOAUTH_PROVIDERS, dropping the APIKEY_PROVIDERS count to 231.
import { test } from "node:test";
import assert from "node:assert/strict";
@@ -53,12 +54,12 @@ test("barrel still exports every catalog + key helpers", () => {
}
});
test("APIKEY_PROVIDERS merges the 6 family files into 232 entries (no loss / no dup)", async () => {
test("APIKEY_PROVIDERS merges the 6 family files into 231 entries (no loss / no dup)", async () => {
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
assert.equal(keys.length, 232);
assert.equal(new Set(keys).size, 232, "duplicate keys after spread-merge");
assert.equal(keys.length, 231);
assert.equal(new Set(keys).size, 231, "duplicate keys after spread-merge");
// the merged object's entry-count equals the sum of the 6 semantic family files; families are a
// strict partition (every provider in exactly one), so the sum must be exactly 232.
// strict partition (every provider in exactly one), so the sum must be exactly 231.
const families: [string, string][] = [
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
@@ -78,7 +79,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 232 entries (no loss / no
seen.add(k);
}
}
assert.equal(famTotal, 232, "families must partition all 232 providers");
assert.equal(famTotal, 231, "families must partition all 231 providers");
});
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {

View File

@@ -0,0 +1,21 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { NOAUTH_PROVIDERS, providerAllowsOptionalApiKey } from "@/shared/constants/providers";
test("uncloseai should be treated as a no-auth provider", () => {
const isNoAuthRegistered = Object.prototype.hasOwnProperty.call(NOAUTH_PROVIDERS, "uncloseai");
const allowsOptionalKey = providerAllowsOptionalApiKey("uncloseai");
assert.equal(
isNoAuthRegistered || allowsOptionalKey,
true,
"uncloseai must be registered in NOAUTH_PROVIDERS or allow an optional API key " +
"so the dashboard doesn't force users to enter a key for a no-auth provider"
);
assert.equal(
isNoAuthRegistered,
true,
"uncloseai must be registered in NOAUTH_PROVIDERS (not just allow an optional key) " +
"so the dashboard renders the NoAuthProviderControls flow"
);
});