From fbcdb28b63deda8bedb999fe7c9f834090045faa Mon Sep 17 00:00:00 2001 From: barckcode Date: Tue, 29 Sep 2026 16:03:09 +0200 Subject: [PATCH 1/2] docs(models): document json_schema 400 on deepseek-v4-flash and tool-name 400 The API now rejects, with a 400 before routing, tool names outside ^[a-zA-Z0-9_-]{1,64}$ (any model) and response_format json_schema on deepseek-v4-flash (json_object keeps working there). - models.mdx (EN/ES): the deepseek-v4-flash card states json_object is supported, json_schema is rejected with a 400, and points to qwen3.6 or gemma4 for schema-constrained output. - openapi.json: ResponseFormat says json_schema is rejected on deepseek-v4-flash while json_object works; Tool.function.name gains the machine-readable pattern and says names outside it are rejected with a 400. - openapiSpec.test.ts: pin the new constraints in the spec and on both model cards; tighten the structured-output example check so it reads the "works on" clause instead of any mention of the model. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/models.mdx | 3 +- src/content/docs/models.mdx | 3 +- src/data/openapi.json | 5 ++- src/lib/openapiSpec.test.ts | 72 +++++++++++++++++++++++++++++++++- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/content/docs-es/models.mdx b/src/content/docs-es/models.mdx index af45f00..67e9973 100644 --- a/src/content/docs-es/models.mdx +++ b/src/content/docs-es/models.mdx @@ -47,7 +47,7 @@ OpenAI y la misma `base URL`. tag="305B MoE" leftLabel="generación de texto, chat y visión" rightLabel="capacidades" - description="Modelo MoE de 305B parámetros, servido en su variante Vision-Exp: acepta imágenes de entrada. Contexto de 1M tokens. Tool calling y razonamiento. Cuota de 3B tokens al mes por miembro." + description="Modelo MoE de 305B parámetros, servido en su variante Vision-Exp: acepta imágenes de entrada. Contexto de 1M tokens. Tool calling y razonamiento. Cuota de 3B tokens al mes por miembro. Salida estructurada: response_format json_object está soportado, json_schema no y se rechaza con un 400. Para salida restringida a un esquema, usa qwen3.6 o gemma4." specs={[ { label: 'Tipo', value: 'MoE (305B total)' }, { label: 'Cuantización', value: 'FP8' }, @@ -60,6 +60,7 @@ OpenAI y la misma `base URL`. 'Tool calling', 'Modo razonamiento', 'Visión (entrada de imagen)', + 'Modo JSON (json_object) · json_schema no soportado (400)', 'Contexto de 1M tokens', 'Generación en streaming (SSE)', ]} diff --git a/src/content/docs/models.mdx b/src/content/docs/models.mdx index 9e6b209..4adfa86 100644 --- a/src/content/docs/models.mdx +++ b/src/content/docs/models.mdx @@ -47,7 +47,7 @@ with the same `base URL`. tag="305B MoE" leftLabel="text generation, chat & vision" rightLabel="capabilities" - description="305B parameter MoE model, served as the Vision-Exp variant: it takes images as input. 1M token context. Tool calling and reasoning. 3B token monthly quota per member." + description="305B parameter MoE model, served as the Vision-Exp variant: it takes images as input. 1M token context. Tool calling and reasoning. 3B token monthly quota per member. Structured output: response_format json_object is supported, json_schema is not and is rejected with a 400. For schema-constrained output, use qwen3.6 or gemma4." specs={[ { label: 'Type', value: 'MoE (305B total)' }, { label: 'Quantization', value: 'FP8' }, @@ -60,6 +60,7 @@ with the same `base URL`. 'Tool calling', 'Reasoning mode (adaptive — not level-adjustable)', 'Vision (image input)', + 'JSON mode (json_object) · json_schema not supported (400)', '1M token context', 'Streaming generation (SSE)', ]} diff --git a/src/data/openapi.json b/src/data/openapi.json index f8a85b7..3c9e415 100644 --- a/src/data/openapi.json +++ b/src/data/openapi.json @@ -1734,7 +1734,8 @@ "properties": { "name": { "type": "string", - "description": "The name of the function to call. Up to 64 characters; letters, digits, underscores and dashes." + "pattern": "^[a-zA-Z0-9_-]{1,64}$", + "description": "The name of the function to call. Up to 64 characters; letters, digits, underscores and dashes. A name outside that pattern is rejected with a `400` before the request reaches the model, on every model." }, "description": { "type": "string", @@ -1795,7 +1796,7 @@ } }, "ResponseFormat": { - "description": "Force structured output. `json_object` guarantees syntactically valid JSON; `json_schema` (with `strict: true`) constrains the output to a schema. Works on `qwen3.6` and `gemma4`.", + "description": "Force structured output. `json_object` guarantees syntactically valid JSON; `json_schema` (with `strict: true`) constrains the output to a schema. `json_schema` works on `qwen3.6` and `gemma4`. On `deepseek-v4-flash` it is rejected with a `400` before the request reaches the model; `json_object` still works there.", "oneOf": [ { "type": "object", diff --git a/src/lib/openapiSpec.test.ts b/src/lib/openapiSpec.test.ts index a77706b..9b0fa75 100644 --- a/src/lib/openapiSpec.test.ts +++ b/src/lib/openapiSpec.test.ts @@ -394,10 +394,19 @@ describe('openapi.json: the model it puts in front of a reader', () => { } }); - /** The structured-output example is allowed to differ, but not to go stale. */ + /** + * The structured-output example is allowed to differ, but not to go stale. + * The ResponseFormat description now also names a model that REJECTS + * `json_schema`, so "the description mentions the model" is no longer + * enough: the model has to be in the sentence that says where it works. + */ it('keeps the structured-output example on a model that supports it', () => { const model = chat.requestBody.content['application/json'].examples.json_schema.value.model; - expect(spec.components.schemas.ResponseFormat.description).toContain(`\`${model}\``); + const works = /`json_schema` works on (.+?)\.(?:\s|$)/.exec( + spec.components.schemas.ResponseFormat.description, + ); + expect(works, 'ResponseFormat no longer says where json_schema works').not.toBeNull(); + expect(works![1]).toContain(`\`${model}\``); }); }); @@ -611,3 +620,62 @@ describe('getting-started guides: /usage parity', () => { } }); }); + +/** + * THE GATEWAY REJECTS TWO REQUEST SHAPES WITH A 400 BEFORE ROUTING: a tool + * name outside ^[a-zA-Z0-9_-]{1,64}$ (any model) and `response_format` + * `json_schema` on `deepseek-v4-flash` (`json_object` still works there). + * A member who hits either should find it in the reference and on the model + * card, in both locales, not learn it from the error. + */ +describe('documented 400s: tool names and structured output', () => { + const schemas = spec.components.schemas as any; + const TOOL_NAME = '^[a-zA-Z0-9_-]{1,64}$'; + + it('publishes the tool-name pattern the gateway enforces', () => { + const name = schemas.Tool.properties.function.properties.name; + expect(name.pattern).toBe(TOOL_NAME); + expect(name.description).toContain('Up to 64 characters; letters, digits, underscores and dashes.'); + expect(name.description).toMatch(/rejected with a `400`/); + expect(name.description).toMatch(/every model/); + }); + + it('the published pattern accepts and rejects what the description says', () => { + const re = new RegExp(TOOL_NAME); + for (const ok of ['get_weather', 'search-web', 'A1', 'x'.repeat(64)]) { + expect(re.test(ok), ok).toBe(true); + } + for (const bad of ['', 'x'.repeat(65), 'files.read', 'mcp:search', 'with space', 'ñandú']) { + expect(re.test(bad), bad).toBe(false); + } + }); + + it('says json_schema is rejected on deepseek-v4-flash and json_object works there', () => { + const description: string = schemas.ResponseFormat.description; + expect(description).toContain( + 'On `deepseek-v4-flash` it is rejected with a `400` before the request reaches the model; `json_object` still works there.', + ); + const works = /`json_schema` works on (.+?)\.(?:\s|$)/.exec(description)!; + expect(works[1]).not.toContain('deepseek-v4-flash'); + }); + + it('states the same on the deepseek-v4-flash model card, in both locales', () => { + const card = (locale: string) => { + const page = readFileSync( + resolve(dirname(fileURLToPath(import.meta.url)), `../content/docs${locale}/models.mdx`), + 'utf-8', + ); + const start = page.indexOf('id="deepseek-v4-flash"'); + expect(start, `${locale || 'en'}: no deepseek-v4-flash card`).toBeGreaterThan(-1); + return page.slice(start, page.indexOf('/>', start)); + }; + const en = card(''); + expect(en).toContain('json_object is supported, json_schema is not and is rejected with a 400'); + expect(en).toContain('use qwen3.6 or gemma4'); + expect(en).toContain('json_schema not supported (400)'); + const es = card('-es'); + expect(es).toContain('json_object está soportado, json_schema no y se rechaza con un 400'); + expect(es).toContain('usa qwen3.6 o gemma4'); + expect(es).toContain('json_schema no soportado (400)'); + }); +}); From fc54e04c842e151ed0fc3ce1f5eeccd3e3ab582b Mon Sep 17 00:00:00 2001 From: barckcode Date: Tue, 29 Sep 2026 16:08:27 +0200 Subject: [PATCH 2/2] docs(models): json_object on deepseek-v4-flash needs the word JSON in the prompt Probed 2026-09-29: without it the upstream answers 400, and the API will reject it with a 400 before routing. Also ES wording 'es compatible'. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/models.mdx | 4 ++-- src/content/docs/models.mdx | 4 ++-- src/data/openapi.json | 2 +- src/lib/openapiSpec.test.ts | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/content/docs-es/models.mdx b/src/content/docs-es/models.mdx index 67e9973..3e3c770 100644 --- a/src/content/docs-es/models.mdx +++ b/src/content/docs-es/models.mdx @@ -47,7 +47,7 @@ OpenAI y la misma `base URL`. tag="305B MoE" leftLabel="generación de texto, chat y visión" rightLabel="capacidades" - description="Modelo MoE de 305B parámetros, servido en su variante Vision-Exp: acepta imágenes de entrada. Contexto de 1M tokens. Tool calling y razonamiento. Cuota de 3B tokens al mes por miembro. Salida estructurada: response_format json_object está soportado, json_schema no y se rechaza con un 400. Para salida restringida a un esquema, usa qwen3.6 o gemma4." + description="Modelo MoE de 305B parámetros, servido en su variante Vision-Exp: acepta imágenes de entrada. Contexto de 1M tokens. Tool calling y razonamiento. Cuota de 3B tokens al mes por miembro. Salida estructurada: response_format json_object es compatible (el prompt debe contener la palabra JSON; si no, se rechaza con un 400), json_schema no y se rechaza con un 400. Para salida restringida a un esquema, usa qwen3.6 o gemma4." specs={[ { label: 'Tipo', value: 'MoE (305B total)' }, { label: 'Cuantización', value: 'FP8' }, @@ -60,7 +60,7 @@ OpenAI y la misma `base URL`. 'Tool calling', 'Modo razonamiento', 'Visión (entrada de imagen)', - 'Modo JSON (json_object) · json_schema no soportado (400)', + 'Modo JSON (json_object, el prompt debe mencionar JSON) · json_schema no soportado (400)', 'Contexto de 1M tokens', 'Generación en streaming (SSE)', ]} diff --git a/src/content/docs/models.mdx b/src/content/docs/models.mdx index 4adfa86..08c6c95 100644 --- a/src/content/docs/models.mdx +++ b/src/content/docs/models.mdx @@ -47,7 +47,7 @@ with the same `base URL`. tag="305B MoE" leftLabel="text generation, chat & vision" rightLabel="capabilities" - description="305B parameter MoE model, served as the Vision-Exp variant: it takes images as input. 1M token context. Tool calling and reasoning. 3B token monthly quota per member. Structured output: response_format json_object is supported, json_schema is not and is rejected with a 400. For schema-constrained output, use qwen3.6 or gemma4." + description="305B parameter MoE model, served as the Vision-Exp variant: it takes images as input. 1M token context. Tool calling and reasoning. 3B token monthly quota per member. Structured output: response_format json_object is supported (the prompt must contain the word JSON, otherwise it is rejected with a 400), json_schema is not and is rejected with a 400. For schema-constrained output, use qwen3.6 or gemma4." specs={[ { label: 'Type', value: 'MoE (305B total)' }, { label: 'Quantization', value: 'FP8' }, @@ -60,7 +60,7 @@ with the same `base URL`. 'Tool calling', 'Reasoning mode (adaptive — not level-adjustable)', 'Vision (image input)', - 'JSON mode (json_object) · json_schema not supported (400)', + 'JSON mode (json_object, the prompt must mention JSON) · json_schema not supported (400)', '1M token context', 'Streaming generation (SSE)', ]} diff --git a/src/data/openapi.json b/src/data/openapi.json index 3c9e415..b378f8f 100644 --- a/src/data/openapi.json +++ b/src/data/openapi.json @@ -1796,7 +1796,7 @@ } }, "ResponseFormat": { - "description": "Force structured output. `json_object` guarantees syntactically valid JSON; `json_schema` (with `strict: true`) constrains the output to a schema. `json_schema` works on `qwen3.6` and `gemma4`. On `deepseek-v4-flash` it is rejected with a `400` before the request reaches the model; `json_object` still works there.", + "description": "Force structured output. `json_object` guarantees syntactically valid JSON; `json_schema` (with `strict: true`) constrains the output to a schema. `json_schema` works on `qwen3.6` and `gemma4`. On `deepseek-v4-flash` it is rejected with a `400` before the request reaches the model; `json_object` still works there, as long as the prompt contains the word JSON (otherwise `400`).", "oneOf": [ { "type": "object", diff --git a/src/lib/openapiSpec.test.ts b/src/lib/openapiSpec.test.ts index 9b0fa75..bf3db4c 100644 --- a/src/lib/openapiSpec.test.ts +++ b/src/lib/openapiSpec.test.ts @@ -653,7 +653,7 @@ describe('documented 400s: tool names and structured output', () => { it('says json_schema is rejected on deepseek-v4-flash and json_object works there', () => { const description: string = schemas.ResponseFormat.description; expect(description).toContain( - 'On `deepseek-v4-flash` it is rejected with a `400` before the request reaches the model; `json_object` still works there.', + 'On `deepseek-v4-flash` it is rejected with a `400` before the request reaches the model; `json_object` still works there, as long as the prompt contains the word JSON (otherwise `400`).', ); const works = /`json_schema` works on (.+?)\.(?:\s|$)/.exec(description)!; expect(works[1]).not.toContain('deepseek-v4-flash'); @@ -670,11 +670,11 @@ describe('documented 400s: tool names and structured output', () => { return page.slice(start, page.indexOf('/>', start)); }; const en = card(''); - expect(en).toContain('json_object is supported, json_schema is not and is rejected with a 400'); + expect(en).toContain('json_object is supported (the prompt must contain the word JSON, otherwise it is rejected with a 400), json_schema is not and is rejected with a 400'); expect(en).toContain('use qwen3.6 or gemma4'); expect(en).toContain('json_schema not supported (400)'); const es = card('-es'); - expect(es).toContain('json_object está soportado, json_schema no y se rechaza con un 400'); + expect(es).toContain('json_object es compatible (el prompt debe contener la palabra JSON; si no, se rechaza con un 400), json_schema no y se rechaza con un 400'); expect(es).toContain('usa qwen3.6 o gemma4'); expect(es).toContain('json_schema no soportado (400)'); });