diff --git a/server/src/chat/gateway.ts b/server/src/chat/gateway.ts index 31e8a56..3ce13dc 100644 --- a/server/src/chat/gateway.ts +++ b/server/src/chat/gateway.ts @@ -214,7 +214,9 @@ export function probeableProvider(prefix: string): boolean { return PROBE_PATTER export function providerReason(text: string): string { let t = text for (let i = 0; i < 3; i++) { - const m = /"message"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(t) + // A complete string first; failing that, a string cut off by an + // upstream truncation, taken to the end of what arrived. + const m = /"message"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(t) ?? /"message"\s*:\s*"((?:[^"\\]|\\.)*)$/.exec(t) if (!m) break t = m[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\') } @@ -305,7 +307,7 @@ export async function probeProvider(prefix: string, sampleModelId: string, key?: const unlockUrl = extractUnlockUrl(r.text) const credentialish = unlockUrl !== null || r.status === 401 || r.status === 403 || /key locked|locked|unauthorized|api key/i.test(r.text) if (credentialish) { - v = { ok: false, kind: 'locked', unlockUrl, message: r.text.slice(0, 200) } + v = { ok: false, kind: 'locked', unlockUrl, message: r.text.slice(0, 2000) } } else { // The gateway masks a provider's own 401 into a generic 400 // (parallelworks/core#19405), so the lock this probe exists for @@ -314,7 +316,7 @@ export async function probeProvider(prefix: string, sampleModelId: string, key?: // for real, whatever the wording; a healthy serve essentially // never fails it once, let alone twice. r = await ping() - if (!r.ok) v = { ok: false, kind: 'unavailable', unlockUrl: extractUnlockUrl(r.text), message: r.text.slice(0, 200) } + if (!r.ok) v = { ok: false, kind: 'unavailable', unlockUrl: extractUnlockUrl(r.text), message: r.text.slice(0, 2000) } } } } catch { /* unreachable is not proof of a lock; leave ok */ } diff --git a/server/test/providerProbe.test.mjs b/server/test/providerProbe.test.mjs index 8b70404..71dfb7a 100644 --- a/server/test/providerProbe.test.mjs +++ b/server/test/providerProbe.test.mjs @@ -115,3 +115,10 @@ test('a marked model carries the reason to the client', async () => { assert.equal(r.impaired[0].reason, 'model not served') assert.equal(r.impaired[0].locked, false) }) + +test('the reason survives a body truncated mid-message', async () => { + const { providerReason } = await import('../dist/chat/gateway.js') + // Exactly what the banner showed: the outer message cut at 200 characters. + const cut = '{"error":{"message":"received error while streaming: {\\"message\\": \\"Requested model is not available and no compliant same-model variant was found.\\", \\"type\\": \\"invalid_reque' + assert.equal(providerReason(cut), 'Requested model is not available and no compliant same-model variant was found.') +}) diff --git a/web/src/layers.css b/web/src/layers.css new file mode 100644 index 0000000..132631a --- /dev/null +++ b/web/src/layers.css @@ -0,0 +1,13 @@ +/* + * Cascade layer order, declared before any stylesheet loads. + * + * The package stylesheets nest their rules in the pw-ui and pw-ai-chat + * layers, except one: the compiled ui stylesheet (0.16) also emits a + * top-level `base` layer that gives every input and textarea a border, + * padding, and background. Declared after the package layers, it + * outranked their utilities, so the chat composer grew a second border + * and the conversation search lost the padding that clears its icon. + * Naming `base` first makes it the lowest layer. The Studio's own inputs + * all carry their own classes and are unaffected. + */ +@layer base, pw-ui, pw-ai-chat; diff --git a/web/src/main.tsx b/web/src/main.tsx index b88a873..5362867 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -9,6 +9,7 @@ import '@fontsource-variable/geist-mono' // equal specificity, so whichever loads last wins. The chat package must come // second: its responsive variants (a two-column starter grid, for one) were // being overridden by the UI package's base utilities. Ours loads last of all. +import './layers.css' import '@parallelworks/ui/styles.css' import '@parallelworks/ui/theme.css' import '@parallelworks/ai-chat/styles.css'