Model APIs and gateways

Connect the model service you already use. First identify its protocol. JavaScript and TypeScript share the runtime API; the quick start gives a complete first call.

Your serviceRecommended connectionCustom executor?
OpenAI / xAI ResponsescreateApiExecutor, default protocol=responsesNo
Responses-compatible gatewaySame adapter with baseURLNo
Compatible /chat/completions gatewayprotocol=chat-completions and baseURLNo
Other protocol, existing SDK, custom authentication headersModelExecutor wrapping your existing callAdapt only model generation
Current Codex modelHost modeNo independent API credentials

“OpenAI-compatible” does not necessarily include Responses. Check the service's actual endpoint. INTENT_MODEL and INTENT_API_KEY are variables the examples read; the library does not automatically read those names.

OpenAI / xAI Responses

import {createApiExecutor} from '@devcodex/intent-runtime/adapters/api';
const executor = createApiExecutor({
  provider: 'openai',
  model: process.env.INTENT_MODEL,
  apiKey: process.env.INTENT_API_KEY,
});

Configure model and apiKey as nonempty strings, then pass executor to new Intent. For xAI, use provider='xai' with its model and credentials.

Connect your gateway

For a gateway exposing /v1/chat/completions, supply the API root without /chat/completions:

const executor = createApiExecutor({
  provider: 'openai',
  protocol: 'chat-completions',
  baseURL: 'https://gateway.example.com/v1',
  apiKey: process.env.INTENT_API_KEY,
  model: process.env.INTENT_MODEL,
});

Replace the hostname, credentials, and model with your service's values. For a Responses-compatible gateway, set protocol='responses'. Pass executor to new Intent({executor}); parsing is unchanged. The openai provider selects a protocol preset; baseURL sends requests to your gateway.

JSON Schema structured output is the default. If the service only supports text output, set format='text' explicitly; it must still return candidate JSON matching the task for core validation. format does not switch protocols or automatically retry another format.

Model parameters

ApiExecutorConfig pathType / requiredDefaultRule
provideropenai or xai / yesnoneExplicit provider
protocolresponses or chat-completions / noresponsesSelects endpoint and request/reply structure; no autodetection
modelnonempty string / yesnoneProvider-supported model name
apiKeynonempty string / yesnoneExcluded from model material and diagnostics
baseURLstring / noOpenAI: https://api.openai.com/v1; xAI: https://api.x.ai/v1Absolute HTTP(S) API root; no credentials/query/fragment; plaintext only for local/private addresses
maxOutputTokenspositive safe integer / no8192Generation limit
maxResponseBytespositive safe integer / no8388608Visible HTTP response-body bytes; separate from candidate limits
formatjson_schema or text / nojson_schemaExplicit choice without automatic fallback

Responses uses /responses; Chat Completions uses /chat/completions. Both set store=false and stream=false, send only current recognition material, omit business tools and implicit history, and do not retry or switch models. 401/403 are authentication failures; 429 is rate limiting. Refusal and incomplete generation have distinct errors; see Recovery.

Success and error bodies are bounded by bytes visible through Response.body after decompression, independently of Content-Length. Exactly the limit is allowed; exceeding it stops reading with RESOURCE_LIMIT/model. Redirects do not forward credentials. Check your actual provider's Schema support and quota; see Compatibility.

Custom ModelExecutor

A ModelExecutor is your adapter for calling the model. Reuse an SDK or HTTP call; you do not need to build another gateway service.

The flow is: user text → Intent prepares a task → your generate calls the model → candidate JSON text → Intent validates and assembles the final result.

generate receivesYour adapter does
instructionsUse as system/task instructions
inputSupply model material, usually JSON.stringify
outputSchemaSet structured output, or instruct unsupported protocols to generate JSON of this shape
signalForward to fetch or a cancellable SDK

Return {outcome:'complete', text:'model-generated candidate JSON'} on success, or refusal/incomplete when appropriate. text is not a final IntentResult. Do not assemble the final result yourself. See ModelExecutor.

The following complete example adapts real Chat Completions with an application header and maps choices[0].message.content into ModelReply. Prefer the built-in adapter for the standard protocol; for another protocol, change the request and text extraction.

Complete TypeScript: custom headers and reply mapping
import {Intent, IntentError} from '@devcodex/intent-runtime';
import type {ModelExecutor, ModelReply} from '@devcodex/intent-runtime';

// Example: adapt a real Chat Completions endpoint with an application header.
// For the standard protocol without custom headers, prefer createApiExecutor.
export function createGatewayExecutor(url: string, model: string, apiKey: string): ModelExecutor {
  return {
    id: 'application-chat-gateway',
    async generate(request): Promise<ModelReply> {
      const response = await fetch(url, {
        method: 'POST', redirect: 'error', signal: request.signal,
        headers: {
          'content-type': 'application/json',
          authorization: 'Bearer ' + apiKey,
          'x-application': 'orders',
        },
        body: JSON.stringify({
          model,
          messages: [
            {role: 'system', content: request.instructions},
            {role: 'user', content: JSON.stringify(request.input)},
          ],
          response_format: {type: 'json_schema', json_schema: {
            name: 'intent_candidate', strict: false, schema: request.outputSchema,
          }},
          max_completion_tokens: 8192,
          stream: false,
          store: false,
        }),
      });
      const reply = await readReply(response);
      if (!reply || typeof reply !== 'object' || !('choices' in reply) || !Array.isArray(reply.choices) || reply.choices.length !== 1) throw new IntentError('MODEL_REQUEST_FAILED');
      const choice = reply.choices[0];
      if (!choice || typeof choice !== 'object' || !choice.message || choice.message.role !== 'assistant') throw new IntentError('MODEL_REQUEST_FAILED');
      if (choice.message.tool_calls?.length || choice.message.function_call) throw new IntentError('MODEL_REQUEST_FAILED');
      if (choice.finish_reason === 'length') return {outcome: 'incomplete'};
      if (choice.finish_reason === 'content_filter' || choice.message.refusal) return {outcome: 'refusal'};
      if (choice.finish_reason !== 'stop' || typeof choice.message.content !== 'string') throw new IntentError('MODEL_REQUEST_FAILED');
      return {outcome: 'complete', text: choice.message.content};
    },
  };
}

// Bound the visible response bytes, including error responses.
async function readReply(response: Response): Promise<unknown> {
  const reader = response.body?.getReader();
  const chunks: Uint8Array[] = [];
  let bytes = 0;
  try {
    if (reader) for (;;) {
      const part = await reader.read();
      if (part.done) break;
      bytes += part.value.byteLength;
      if (bytes > 8388608) throw new IntentError('RESOURCE_LIMIT', {stage: 'model'});
      chunks.push(part.value);
    }
  } finally {
    if (reader) {await reader.cancel().catch(() => {}); reader.releaseLock();}
  }
  if (response.status === 401 || response.status === 403) throw new IntentError('MODEL_AUTH_FAILED');
  if (response.status === 429) throw new IntentError('MODEL_RATE_LIMITED');
  if (!response.ok) throw new IntentError('MODEL_REQUEST_FAILED');
  const body = new Uint8Array(bytes);
  let offset = 0;
  for (const chunk of chunks) {body.set(chunk, offset); offset += chunk.byteLength;}
  return JSON.parse(new TextDecoder('utf-8', {fatal: true}).decode(body));
}

export async function main(): Promise<void> {
  const {INTENT_GATEWAY_URL: url, INTENT_MODEL: model, INTENT_API_KEY: apiKey} = process.env;
  if (!url || !model || !apiKey) throw new Error('Set INTENT_GATEWAY_URL, INTENT_MODEL and INTENT_API_KEY.');
  const intent = new Intent({language: 'en', executor: createGatewayExecutor(url, model, apiKey)});
  try {console.log(await intent.parse({input: 'Analyze the payments module. Do not modify files.'}));}
  finally {await intent.dispose();}
}

await main();

Save as custom-executor-en.ts in an ESM project and set INTENT_GATEWAY_URL, INTENT_MODEL, INTENT_API_KEY. Use your TypeScript build, or:

npm install -D typescript @types/node
npx tsc custom-executor-en.ts --outDir out --target ES2022 --module NodeNext --moduleResolution NodeNext --strict --skipLibCheck
node out/custom-executor-en.js

INTENT_GATEWAY_URL is the complete endpoint, for example https://gateway.example.com/v1/chat/completions. This example calls fetch(url) and does not append a path. Set "type":"module" in package.json. It forwards signal to local fetch and bounds responses; remote cancellation follows your provider protocol. Preserve the candidate format and keep business execution outside recognition.