{"slug": "openais-decisions-api-is-in-public-beta-build-a-typed-router-in-typescript", "title": "OpenAI’s Decisions API Is in Public Beta. Build a Typed Router in TypeScript.", "summary": "OpenAI released its Decisions API in public beta on October 6, 2026, offering GPT-6 Luna with three output types — predicates, choices and scores — and claiming decisions can run up to 10× faster than GPT-6 Luna through the Responses API. A developer published a TypeScript router that validates the API's typed output at the application boundary, checking for a single named answer, handling refusals and unexpected types, and routing unknown or duplicate answers to a review queue instead of dispatching them directly. The demo runs on Node.js 22.20.0 with built-in type stripping and uses ten invented fixtures rather than live API calls.", "body_md": "**October 7, 2026 — news:** OpenAI made the Decisions API available in public beta on October 6. The announcement names GPT-6 Luna and three outputs: predicates, choices and scores. OpenAI claims decisions can be **up to 10× faster than GPT-6 Luna through the Responses API**. That is a vendor claim, not a measurement from this build. [Primary announcement](https://community.openai.com/t/decisions-api-is-now-available-in-public-beta/1403877).\n\nThe engineering idea I care about is smaller than an agent loop: hand the model a bounded question, then let application code decide what happens next.\n\nA support message arrives. You need a queue, not a paragraph.\n\nThe dangerous implementation is one line:\n\n```\ndispatch(answer.choice);\n```\n\nThe useful implementation has a boundary. It knows the expected question name, the allowed routes, the answer type, and what to do when the model refuses or the response is unusable.\n\nLet's build that boundary in TypeScript.\n\n**Honesty note:** this is a local consumer-side demo with ten invented response fixtures. It makes no OpenAI API requests and needs no API key. It tests our routing policy, not GPT-6 Luna's accuracy, calibration, speed or refusal rate. The threshold is illustrative.\n\nThe [October 6 API changelog](https://developers.openai.com/api/docs/changelog) records the beta release at `/v1/decisions`. This is an availability update following the earlier preview, not a claim that the concept first appeared yesterday.\n\nThe current [Decisions guide](https://developers.openai.com/api/docs/guides/decisions) says the beta supports `gpt-6-luna` and text/image inputs. A predicate estimates whether a condition is true; a choice selects from your fixed values; a score evaluates ordered levels. A question's name identifies its answer. Refusals need their own handling.\n\nFor a support router, a choice is a natural fit. The result can assign work to billing, technical support, or a review queue. My design keeps refunds and other consequential actions outside that router.\n\nTyped output shrinks the space of normal answers. It does not remove the need to validate external data or define failure behavior.\n\nWe accept `unknown`, rather than pretending the network response already has a trusted TypeScript type.\n\nThen we check:\n\n`department`.\nNotice the order. A high confidence cannot rescue an unknown route. A duplicate answer cannot silently win because it happened to appear first.\n\nThe code intentionally validates only the fields our application uses. It is not a complete validator for every field in the API response.\n\nSave this as `router.ts`. I ran it on Node.js 22.20.0 using built-in TypeScript type stripping. No npm packages are required for execution.\n\n```\nnode router.ts\n```\n\nType stripping runs TypeScript syntax; it does not perform a type check.\n\n``` python\nimport assert from 'node:assert/strict';\n\nconst routes = ['billing', 'technical', 'other'] as const;\ntype Route = typeof routes[number];\ntype Result = { queue: Route | 'review'; reason: string };\nconst object = (x: unknown): x is Record<string, unknown> =>\n  typeof x === 'object' && x !== null && !Array.isArray(x);\n\n// This validates the application boundary, not every API response field.\nfunction routeDecision(raw: unknown): Result {\n  const review = (reason: string): Result => ({ queue: 'review', reason });\n  if (!object(raw) || !Array.isArray(raw.answers))\n    return review('malformed envelope');\n  const matches = raw.answers.filter(\n    x => object(x) && x.name === 'department'\n  );\n  if (matches.length !== 1) return review('missing or duplicate answer');\n  const answer = matches[0] as Record<string, unknown>;\n  if (answer.type === 'refusal') return review('model refusal');\n  if (answer.type !== 'choice') return review('unexpected answer type');\n  if (typeof answer.choice !== 'string' ||\n      !routes.includes(answer.choice as Route))\n    return review('unknown route');\n  const confidence = answer.confidence;\n  if (typeof confidence !== 'number' || !Number.isFinite(confidence) ||\n      confidence < 0 || confidence > 1)\n    return review('invalid confidence');\n  // Illustrative policy only; choose a threshold using labeled evaluations.\n  if (confidence < .8) return review('below local threshold');\n  if (answer.choice === 'other') return review('fallback category');\n  return { queue: answer.choice as Route, reason: 'validated choice' };\n}\n\nconst envelope = (answer: unknown) => ({ answers: [answer] });\nconst choice = (value: string, confidence: number) =>\n  envelope({ type: 'choice', name: 'department', choice: value, confidence });\nconst cases: [string, unknown, Result][] = [\n  ['valid', choice('billing', .94), { queue: 'billing', reason: 'validated choice' }],\n  ['low', choice('technical', .6), { queue: 'review', reason: 'below local threshold' }],\n  ['fallback', choice('other', .95), { queue: 'review', reason: 'fallback category' }],\n  ['refusal', envelope({ type: 'refusal', name: 'department' }),\n    { queue: 'review', reason: 'model refusal' }],\n  ['unknown', choice('refund_now', .99), { queue: 'review', reason: 'unknown route' }],\n  ['nan', choice('billing', NaN), { queue: 'review', reason: 'invalid confidence' }],\n  ['missing', { answers: [] }, { queue: 'review', reason: 'missing or duplicate answer' }],\n  ['duplicate', { answers: [\n    { type: 'choice', name: 'department', choice: 'billing', confidence: .9 },\n    { type: 'choice', name: 'department', choice: 'technical', confidence: .9 }\n  ] }, { queue: 'review', reason: 'missing or duplicate answer' }],\n  ['wrong-type', envelope({ type: 'predicate', name: 'department', probability: .9 }),\n    { queue: 'review', reason: 'unexpected answer type' }],\n  ['malformed', null, { queue: 'review', reason: 'malformed envelope' }],\n];\nfor (const [name, fixture, expected] of cases) {\n  const result = routeDecision(fixture);\n  assert.deepEqual(result, expected);\n  console.log(name + ': ' + result.queue + ' (' + result.reason + ')');\n}\nconsole.log('All ' + cases.length + ' fixture assertions passed.');\nvalid: billing (validated choice)\nlow: review (below local threshold)\nfallback: review (fallback category)\nrefusal: review (model refusal)\nunknown: review (unknown route)\nnan: review (invalid confidence)\nmissing: review (missing or duplicate answer)\nduplicate: review (missing or duplicate answer)\nwrong-type: review (unexpected answer type)\nmalformed: review (malformed envelope)\nAll 10 fixture assertions passed.\n```\n\nNine review outcomes do not mean the model fails 90% of the time. We deliberately constructed nine exceptional inputs. This is a boundary test, not a representative dataset.\n\nThe missing and duplicate cases are especially useful. Neither should become a default billing route. The NaN case catches a JavaScript detail: numeric type alone does not establish a usable number.\n\nThe refusal case gets a distinct reason. Keeping reasons explicit lets you measure whether review load comes from uncertain classifications, provider refusals, invalid data, or a taxonomy that needs work.\n\nThe [API reference](https://developers.openai.com/api/reference/resources/decisions/methods/create) defines a request with `model`, `input` and `questions`. For this router, the named question would be a choice called `department`, with billing, technical and other as the allowed values.\n\nThe corresponding application flow is straightforward:\n\nThis demo starts at step two. It deliberately does not include an untested SDK integration or disguise fixture answers as live model responses.\n\nIn a real service, I would also catch transport failures outside this function, apply a request deadline, and record model, question configuration version, selected queue and review reason. A successful HTTP request and a usable routing answer are different observations.\n\nThe 0.80 threshold is a teaching value. I would choose a real threshold with labeled validation data, then evaluate on a separate held-out set. I would count misroutes per queue, fallback frequency and the review workload the policy creates.\n\nFor the speed claim, I would compare the same task and evidence across endpoints, reporting end-to-end p50 and p95 latency alongside quality. A faster answer is valuable only if the downstream routing remains useful. No latency chart appears here because this build made no live calls.\n\nI would also version the taxonomy. If technical support splits into product and infrastructure, that is a routing contract change. It deserves new fixtures and evaluation, even if the model stays the same.\n\nMy takeaway from the beta launch: a model can provide a compact judgment while the application keeps routing behavior explicit and testable.\n\nI'm building [Roster](https://get-roster.com), AI employees that do real work. These are the boundaries I care about around their workflows: what answer did the model return, what did the software validate, and where did the work actually go?\n\nWhich narrow decision in your system is still hiding inside a generated paragraph?", "url": "https://wpnews.pro/news/openais-decisions-api-is-in-public-beta-build-a-typed-router-in-typescript", "canonical_source": "https://dev.to/bobbyhalljr/openais-decisions-api-is-in-public-beta-build-a-typed-router-in-typescript-ob7", "published_at": "2026-10-07 16:37:11+00:00", "updated_at": "2026-10-07 16:47:29.978113+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "large-language-models", "developer-tools", "ai-agents"], "entities": ["OpenAI", "Decisions API", "GPT-6 Luna", "Responses API", "Node.js", "TypeScript"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/openais-decisions-api-is-in-public-beta-build-a-typed-router-in-typescript", "markdown": "https://wpnews.pro/news/openais-decisions-api-is-in-public-beta-build-a-typed-router-in-typescript.md", "text": "https://wpnews.pro/news/openais-decisions-api-is-in-public-beta-build-a-typed-router-in-typescript.txt", "jsonld": "https://wpnews.pro/news/openais-decisions-api-is-in-public-beta-build-a-typed-router-in-typescript.jsonld"}}