diff --git a/src/content/docs-es/models.mdx b/src/content/docs-es/models.mdx index af45f00..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." + 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,6 +60,7 @@ OpenAI y la misma `base URL`. 'Tool calling', 'Modo razonamiento', 'Visión (entrada de imagen)', + '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 9e6b209..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." + 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,6 +60,7 @@ with the same `base URL`. 'Tool calling', 'Reasoning mode (adaptive — not level-adjustable)', 'Vision (image input)', + '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 f8a85b7..b378f8f 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, 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 a77706b..bf3db4c 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, 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'); + }); + + 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 (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 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)'); + }); +});