# XXXRouter full documentation > API documentation. Demonstration environment; production integration acceptance is pending. # Quickstart > Implementation reference · 2026-10-26 · Configure a deployment before sending traffic. The application uses server-side authentication, balances and requests. `https://xxxrouter.com` is the official service domain; for local testing, replace it with your deployment origin. The service has not yet been deployed and integration certification is not complete. Same-origin `/v1` is supported through the gateway. Models start disabled; operators must configure credentials, certify routes and set prices before enabling them. This reference is not evidence of deployment or completed integration testing. 1. Sign in to the console and review your USD balance. 2. Create an API key. Copy it once and store it in your server's secret manager. 3. Review the model's response mode and effective price. 4. Call the API from your backend. Track the returned request ID in Usage & Logs. ```sh export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' curl https://xxxrouter.com/v1/models \ -H "Authorization: Bearer $XXXROUTER_API_KEY" ``` ```sh curl https://xxxrouter.com/v1/chat/completions \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-chat-request-001' \ -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Hello"}],"max_tokens":256,"stream":false}' ``` Chat follows an OpenAI-style contract. Image APIs support both direct results and task responses; they are not universally drop-in OpenAI Images replacements. The console Playground uses the same prices and accounting policy as external API calls, through the signed-in session, attributed separately from API keys. Continue: [authentication](/docs?page=auth&lang=en), [model capabilities](/docs?page=models&lang=en), [images](/docs?page=images&lang=en), [errors](/docs?page=errors&lang=en). Machine interfaces: [OpenAPI](/openapi.json), [llms.txt](/llms.txt), [full text](/llms-full.txt). --- # Authentication Send `Authorization: Bearer ` on generation, task and key requests. Model catalog GET endpoints permit anonymous public pricing; an optional valid key selects account pricing. Never place keys in query strings, public repositories, client bundles, shared screenshots, or browser storage. Create a real key in an authenticated, verified console account. A live key will be shown once at creation. Subsequent views show its prefix/suffix and immutable ID. The server stores a keyed one-way digest, not recoverable plaintext. Key management requires a console session; an inference key cannot mint other keys or change account billing. To rotate a key: create a replacement, deploy it, confirm traffic, then revoke the original. Pause is reversible; revocation is permanent. Expiry is evaluated in UTC on each new request. Already accepted tasks retain their billing and history after pause, expiry, or revocation. Use another authorized key from the same account or the console to inspect them. `GET /v1/key` returns metadata for the current key, including effective restrictions and remaining spending allowance. It never returns the secret. Errors do not reveal whether a guessed key belongs to another account. [Key controls](/docs?page=keys&lang=en) · [Errors](/docs?page=errors&lang=en) --- # Models & pricing `GET /v1/models` lists models available to your account. `GET /v1/models/{id}` returns capabilities, parameters, response mode, availability, and effective price. Public model IDs are platform-owned names without routing prefixes; for example, `/v1/models/seedream-4.5`. Routing and infrastructure metadata are internal and are not returned by the public model API. | Demo model ID | Output | Response mode | |---|---|---| | `seedream-4.5` | Image | `sync` | | `nano-banana-pro` | Image | `async` | | `flux-2-pro` | Image | `async` | | `kling-3.0` | Video | `async` | | `deepseek-v4-flash` | Text | `stream` catalog mode; JSON or SSE selected by `stream` | These are public platform model IDs, not guarantees of configured or certified live availability. A model's response mode is explicit catalog metadata, never inferred from a URL substring or programming-language `async` keyword. Model capability validation occurs before any hold or provider submission. Prices are denominated in USD. Image rates depend on supported parameters, video rates on duration in this release, and LLM rates separately price input and output tokens. A request stores the effective price version at acceptance; later catalog edits do not change it. Estimates are not settled costs. [Image protocol](/docs?page=images&lang=en) · [Billing](/docs?page=billing&lang=en) ## Effective-price priority Use exactly one matching rule: user × model override, then price-group × model override, then price-group default discount, then public price. Overrides may be a fixed price or a discount within that model's allowed range. Rules do not stack. Below-cost prices require explicit operator confirmation; unknown supplier cost must not display a fabricated margin. Freeze the selected rule, units and version at request acceptance. The model `parameters` list advertises normalized top-level fields: images use `size`, `n`, `image`; videos use `duration`, `aspect_ratio`, `image`; chat uses `temperature`, `max_tokens`, `stream`, `stream_options`. This is not an arbitrary passthrough dictionary. Seedream 4.5 examples use `2048x2048`; production parameter limits must be verified for each model. UI sample choices are not guarantees of live capabilities. Model JSON uses name, modality (llm/image/video), mode/response_mode (stream/sync/async), parameters, sizes, supportsImage and enabled. pricing is an object with currency, price and nullable outputPrice (six-decimal USD strings); price/outputPrice are numeric USD fields. A disabled model is not callable. Seed prices require operator review; unknown costs are never presented as margins. --- # Chat completions `POST /v1/chat/completions` accepts `model`, `messages`, `max_tokens`, `temperature`, `stream`, and optional `stream_options: {"include_usage": true}`. The v0.1 contract covers text messages with `system`, `user`, and `assistant` roles. Tool calls, multimodal chat, embeddings, and the Responses API are outside this release contract. ```json {"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Explain HTTP in one sentence."}],"max_tokens":256,"temperature":0.7,"stream":true,"stream_options":{"include_usage":true}} ``` A non-streaming result contains `id`, `object: "chat.completion"`, `model`, `choices`, and `usage`. Streaming returns `text/event-stream`; each data event contains a completion chunk with `choices[].delta`. With `stream_options.include_usage=true`, the final upstream usage event, when supplied, precedes `data: [DONE]`. Record `X-Request-ID` from response headers. Do not parse SSE as one JSON document. The gateway must not insert a JSON-only system prompt, silently switch the model, or replay a charged completion to repair output. Disconnecting or pressing Stop does not erase tokens already generated. Final billing uses observed usage; missing authoritative usage stays pending reconciliation, never silently becomes zero. Each route requires integration certification before being enabled. [Billing](/docs?page=billing&lang=en) · [Production](/docs?page=production&lang=en) All generation POST requests require an Idempotency-Key (1–128 printable ASCII characters). Use a fresh value for a new logical request and the same value only for exact replay. max_tokens is 1–4096; temperature is 0–2. Stream replay does not restart or replay a charged upstream stream. --- # Image generation `POST /v1/images/generations` has two declared response modes, selected by the model catalog. Always inspect the model metadata and handle the documented response. Both modes accept `model`, `prompt`, optional top-level `image` reference URL, `size`, and `n`. These are normalized platform fields; provider adapters translate them to a certified model schema. ```sh curl https://xxxrouter.com/v1/images/generations \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-image-request-001' \ -d '{"model":"seedream-4.5","prompt":"A ceramic cup in natural light","size":"2048x2048","n":1}' ``` ## Synchronous model: HTTP 200 ```json {"request_id":"req_demo_sync","model":"seedream-4.5","response_mode":"sync","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/cup.png"}],"billing":{"status":"settled","currency":"USD","amount":"0.030000"}} ``` The image arrives in the original HTTP response. No provider task ID or follow-up task polling is required. The amount above is a demo, not a quote. Slow synchronous execution remains synchronous; a client timeout is not proof of failure or permission to resubmit. ## Asynchronous model: HTTP 202 Select `nano-banana-pro` or `flux-2-pro` to receive: ```json {"request_id":"req_demo_async","task_id":"task_demo_01","model":"nano-banana-pro","response_mode":"async","status":"running"} ``` Poll [the task endpoint](/docs?page=tasks&lang=en) until terminal. Do not read `202` as completed output. A shared image route does not imply all models are OpenAI Images compatible. Configured routes submit real requests. Missing credentials never trigger a simulated paid result. [Reference inputs](/docs?page=uploads&lang=en) · [Errors](/docs?page=errors&lang=en) The current service supports n=1 only. image is allowed only when the model parameters include it. Successful responses return the original generated-media URL unchanged. The original host controls availability, expiry and browser cross-origin behavior. Save results before expiry; querying a completed task returns the saved URL and does not renew it. Request metadata and result URLs may be retained for recovery; generated-media bytes are never retained. --- # Video generation `POST /v1/videos/generations` returns HTTP 202 and a task. The public model ID is `kling-3.0`; live availability requires a configured and certified route. ```json {"model":"kling-3.0","prompt":"A slow camera movement around a ceramic cup","duration":5,"aspect_ratio":"16:9"} ``` Send an `Idempotency-Key` for each logical submission. Store `task_id` and poll `/v1/tasks/{task_id}`. Successful results include video URL, media type, and final billing. Do not promise cancellation or a supplier progress percentage unless the provider actually supports it. Exact progress is omitted without reliable evidence. A quoted duration is a pricing input, not a guarantee of exact generated duration. Before launch each route must document how it bills output duration and parameter combinations. No automatic cross-model fallback is assumed. [Tasks](/docs?page=tasks&lang=en) · [Billing](/docs?page=billing&lang=en) Normalized top-level parameters are `duration`, `aspect_ratio`, and optional `image`. The current schema accepts 5/10 seconds and 16:9, 9:16 or 1:1; check model availability before calling. There is no `resolution` or nested `parameters` field in this draft request. Successful responses return the original generated-media URL unchanged. The original host controls availability, expiry and browser cross-origin behavior. Save results before expiry; querying a completed task returns the saved URL and does not renew it. Request metadata and result URLs may be retained for recovery; generated-media bytes are never retained. --- # Tasks & idempotency `GET /v1/tasks/{id}` reads an accepted image/video task. Authentication and account ownership are checked on every read. A different valid key in the same account may inspect the task; the original key ID remains the immutable usage attribution. States: `submitting → running → succeeded | failed`. An uncertain outcome uses request `status: "unknown"` and `billing.status: "reconciling"`; it must not be resubmitted automatically. Successful task output and billing status are separate: a result can be available while settlement is being reconciled. ```json {"task_id":"task_demo_01","request_id":"req_demo_async","model":"nano-banana-pro","response_mode":"async","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/image.png","media_type":"image"}],"billing":{"status":"settled","currency":"USD","amount":"0.150000"}} ``` Poll with a bounded interval (start at 2 seconds and back off up to 10 seconds); respect `Retry-After`. GET retries do not create new work. A lost POST response is different: reuse the same idempotency key and identical payload. Same account + endpoint + key + request fingerprint returns the original record; a different payload returns `409 IDEMPOTENCY_CONFLICT`. Idempotency records are persisted in MySQL. Do not assume a new day makes it safe to reuse an old key or resubmit an uncertain request. For synchronous images the replay returns the recorded direct result when available. In-progress or uncertain synchronous requests return HTTP `409` with the current request record; no second upstream submit occurs. Synchronous request IDs must not be polled through `/tasks` as if they were task IDs. [Errors](/docs?page=errors&lang=en) · [Production](/docs?page=production&lang=en) Successful responses return the original generated-media URL unchanged. The original host controls availability, expiry and browser cross-origin behavior. Save results before expiry; querying a completed task returns the saved URL and does not renew it. Request metadata and result URLs may be retained for recovery; generated-media bytes are never retained. --- # Balance & billing The account has one prepaid USD balance. Key spending limits constrain consumption; they are not separate wallets. Display available, held, and settled amounts distinctly. Store currency in integer microdollars or exact decimals, never binary floating-point. The approved trial is $0.20 in promotional credit, valid for 7 days. It is separate from paid funds and is not cash. The server enforces eligibility and one-time granting. Verify your email and complete the challenge; grant expiry is enforced independently from paid funds. Before accepting work, reserve sufficient account funds and key allowance atomically. At completion settle the actual amount and release the difference. Failures with no billable usage release the hold. Unknown supplier outcomes stay under reconciliation rather than creating a second charge. A timeout or stopping a stream does not establish zero usage. Payblis and NOWPayments are the only checkout providers in scope. Configured payment channels create hosted checkout orders; missing credentials return an unavailable state. A success redirect never credits an account. Verified callbacks and independent reconciliation must check currency, amount, provider reference, and order identity before a unique ledger entry is posted. Implemented order states are `creating`, `pending`, `confirming`, `paid`, `underpaid`, `expired`, and `failed`. Partial payments remain `underpaid` until confirmed as fully paid. A refund API and automated refund processing are not implemented in this release. Any future refund must append compensating ledger entries rather than rewrite historical transactions. [Key controls](/docs?page=keys&lang=en) · [Production](/docs?page=production&lang=en) ## Trial eligibility and spending order Verify the account email and complete the verification challenge before claiming the $0.20 grant. Claiming is idempotent and rate limited. The grant expires exactly 7 days after issuance; eligible promotional funds are used before paid funds. Trial requests use the same effective price and key/account limits. Holds preserve funding-source attribution. If a hold is released after the grant expires, its promotional portion returns to expired promotional balance, not spendable paid balance. Expiry never removes paid money. Financial ledger history is permanent and independent of 30-day request logs. --- # API key controls Manage keys in the console; `GET /v1/key` is the read-only self-inspection API. Every key has an immutable ID, display name, masked secret, status, creation time, optional expiry, spending allowance, and usage attribution. The account balance and key allowance must both cover a request. Outstanding holds count against both, so parallel calls cannot overspend by checking stale totals. Unlimited key allowance still requires account funds. Lowering a cap below occupied allowance prevents new work; it does not erase prior spend or cancel accepted tasks. Pause rejects new calls and can be reversed. Revoke rejects new calls permanently. An expired key cannot call the API even if its owner has money. Existing tasks continue and retain the submitting key ID. A replacement key starts a distinct usage history. Playground authenticates through the signed-in session, shown separately as source `playground`; it never exposes or requires a user's permanent key. Account charts aggregate Playground and external API usage. Filtered key charts show only that key; archived/revoked keys remain available in historical filters. The server returns the plaintext secret once; subsequent views cannot recover it. [Authentication](/docs?page=auth&lang=en) · [Billing](/docs?page=billing&lang=en) ## Restriction fields Each key selects one spending cap and one period: lifetime, daily, weekly, or monthly. You may edit the period; the applicable settled spend is recalculated from the current period, without deleting history. Daily, weekly and monthly periods reset at UTC midnight, Monday 00:00 UTC and the first day of the month 00:00 UTC. All outstanding holds count toward the active cap, including holds accepted before a period boundary, so in-flight work does not bypass a newly reset limit. Upon settlement, spend belongs to its request acceptance time; lifetime includes all history. Model allowlists, RPM and IPv4/IPv6 addresses and CIDR allowlists add independent gates. An empty model/IP allowlist means unrestricted; the UI labels that explicitly. Both address families are supported. Account platform ceilings still apply. Remaining key allowance is not account balance. Service enforcement requires correctly configured proxy trust in production. GET /v1/key returns id, name, mask, enabled, revoked, limit, period, expiresAt, models, rpm, ips, spent, reserved and nextReset. Key monetary fields use USD numbers. limit=null means no key spending cap; limit=0 permits no positive spending. Account balance still applies. No secret is returned. --- # Errors & limits ```json {"error":{"code":"KEY_BUDGET_EXCEEDED","message":"This key has insufficient spending allowance.","request_id":"req_demo_error"}} ``` | HTTP | Code | Recovery | |---|---|---| | 422 | `INVALID_INPUT` | Correct the input before resubmitting. | | 401 | `INVALID_API_KEY`, `API_KEY_INACTIVE` | Use a valid credential. | | 403 | `MODEL_NOT_ALLOWED`, `IP_NOT_ALLOWED` | Review key restrictions in the console. | | 402 | `INSUFFICIENT_BALANCE`, `KEY_BUDGET_EXCEEDED` | Top up or change the authorized budget. | | 404 | `MODEL_NOT_FOUND`, `REQUEST_NOT_FOUND` | Check the ID and ownership. | | 409 | `IDEMPOTENCY_CONFLICT` | Use the original payload or a new logical request. | | 409 | In-progress request record or `RESPONSE_CACHE_EXPIRED` | Wait/reconcile; do not buy a duplicate generation. | | 422 | `UNSUPPORTED_PARAMETER` | Correct the model/input choice. | | 429 | `RATE_LIMITED` | Respect Retry-After and reduce traffic. | | 502/503 | `MODEL_UNAVAILABLE` | Inspect request status before retrying a submission. | Messages are sanitized; supplier balance, merchant secrets, and raw upstream stack traces are never shown. `X-Request-ID` supports troubleshooting. Unknown provider errors must not be falsely labeled as content-policy violations. After SSE headers are sent, an error is an SSE error event followed by closure, not a second HTTP response. [Idempotency](/docs?page=tasks&lang=en) · [Production](/docs?page=production&lang=en) --- # Production integration This reference accompanies the service implementation; deployment and integration certification are separate steps. Before launching real traffic, certify each provider route, parameter schema, price calculation, streaming usage, payment signature, and failure-recovery path against a separate test environment. - Use server-side keys, TLS, explicit timeouts, bounded polling, and request IDs. - Persist idempotency records before submission; a paid upstream POST is never blindly retried after an ambiguous transport failure. - Keep wallet ledger, holds, key budgets, and payment settlement transactional in MySQL. Use DynamoDB for request event/query projections and 30-day request-log retention, not as a competing wallet authority. - Submit directly after persisting the hold and claim. Return an asynchronous task only after receiving its upstream ID. Task GET polls and settles; a periodic reconciler checks unattended tasks and retries failed log writes. - Default logs contain metadata only and expire after 30 days. Do not persist prompts, chat content, input files, generated files, Authorization headers, or payment secrets in these logs. - Ledger and payment records have a separate retention policy; deleting request metadata must not delete financial history. Production retention requirements must be finalized before launch. - Alarm on reconciliation backlog, stuck holds, duplicate settlement attempts, task latency, log retry backlog, provider errors, and payment mismatch. Configure credentials, DNS/TLS, workload permissions and integration acceptance before launch. [Tasks](/docs?page=tasks&lang=en) · [Billing](/docs?page=billing&lang=en) MySQL owns task coordination, financial transactions and pending log retry records. Write metadata directly to DynamoDB with account/key/time projections, date buckets and LOG_SHARDS; filter expired records during queries. A lightweight reconciler polls unattended tasks and retries log writes. Web, gateway and reconciler are separate deployments. Successful responses return the original generated-media URL unchanged. The original host controls availability, expiry and browser cross-origin behavior. Save results before expiry; querying a completed task returns the saved URL and does not renew it. Request metadata and result URLs may be retained for recovery; generated-media bytes are never retained. --- # Reference URLs & temporary inputs When model metadata supports `image`, supply one top-level HTTPS reference image URL in an image or video request. Browser `blob:` URLs cannot be accessed by the service. Multiple reference images are not part of the current normalized request. For image-to-image or image-to-video, pass a public HTTPS URL in `image` when the model supports reference images. The public inference API takes URLs, not file bytes. Playground can upload temporary input images or videos to private R2 storage; only signed-in, verified users can use it. Generated results are never downloaded, stored or rehosted by XXXRouter. Temporary Playground inputs support PNG/JPEG/WebP up to 10 MiB and MP4/WebM up to 50 MiB, with 20 upload attempts per minute per account. Read links last at most 1 hour and are not renewed. A one-day object expiration rule cleans up files asynchronously; it is not an exact physical deletion deadline. An image URL can be filled into a supported reference field. Video URLs are usable only when the selected model declares video-input support; they cannot be passed as reference images. Keep your own externally hosted URLs accessible throughout processing. ```json {"model":"seedream-4.5","prompt":"A small blue house","image":"https://example.com/reference.png","n":1,"size":"2048x2048"} ``` Successful responses return the original generated-media URL unchanged. The original host controls availability, expiry and browser cross-origin behavior. Save results before expiry; querying a completed task returns the saved URL and does not renew it. Request metadata and result URLs may be retained for recovery; generated-media bytes are never retained. --- # Python integration Run these examples on your server. They use the language runtime HTTP client, not a platform SDK. No request is sent until you run a command. Models must be configured and enabled before use. ## Setup Runtime: **Python 3.11+**. [Download complete source](/examples/python.py). Set your deployment base URL, including `/v1` exactly once, and a server-side API key. `https://xxxrouter.com` is the official service domain; for local testing, replace it with your deployment origin. The service is not yet deployed and integration certification is not complete. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Run one path `chat` reads SSE deltas and final usage; `image` receives a synchronous image; `async-image` and `video` handle HTTP 202 and query the returned task. Polling is bounded to approximately five minutes plus any in-flight read; a timeout preserves the task for later reads. ```sh python3 python.py chat # python3 python.py image # python3 python.py async-image # python3 python.py video ``` ## Idempotency and failure handling A new run creates a new idempotency key and can incur a new charge. In an application, persist that key with the exact body before submitting. Set `XXXROUTER_REQUEST_ID` only to resume the same logical request with the same model, payload, API key and source. Do not rerun with a new key after a transport failure. These clients perform one generation POST and never retry it automatically. Record the response `X-Request-ID`. HTTP errors retain the response body for diagnosis. A task with `status=unknown` stops automatic polling and requires later read/reconciliation; it is not a successful or free request. A stream error event or missing `[DONE]` is an interruption. Missing usage is not zero cost. Stopping the connection does not cancel accepted work. Do not log API keys or signed asset URLs. ## Image inputs and result files For image-to-image or image-to-video, pass a public HTTPS URL in `image` when the model supports reference images. The public inference API takes URLs, not file bytes. Playground can upload temporary input images or videos to private R2 storage; only signed-in, verified users can use it. Generated results are never downloaded, stored or rehosted by XXXRouter. Successful responses return the original generated-media URL unchanged. The original host controls availability, expiry and browser cross-origin behavior. Save results before expiry; querying a completed task returns the saved URL and does not renew it. Request metadata and result URLs may be retained for recovery; generated-media bytes are never retained. ## Complete example The code is an integration example, not proof that a live model or payment channel is enabled. Do not place it in a browser bundle. ```python """Python 3.11+, standard library only. No automatic generation retries.""" import json import os import sys import time import urllib.error import urllib.request import uuid BASE = os.environ['XXXROUTER_BASE_URL'].rstrip('/') # Include /v1 exactly once. KEY = os.environ['XXXROUTER_API_KEY'] MODE = sys.argv[1] if len(sys.argv) > 1 else 'chat' CASES = { 'chat': ('chat/completions', {'model': 'deepseek-v4-flash', 'messages': [{'role': 'user', 'content': 'Explain HTTP in one sentence.'}], 'max_tokens': 256, 'stream': True, 'stream_options': {'include_usage': True}}), 'image': ('images/generations', {'model': 'seedream-4.5', 'prompt': 'A ceramic cup in natural light', 'size': '2048x2048', 'n': 1}), 'async-image': ('images/generations', {'model': 'nano-banana-pro', 'prompt': 'A ceramic cup in natural light', 'size': '1024x1024', 'n': 1}), 'video': ('videos/generations', {'model': 'kling-3.0', 'prompt': 'A slow camera move across a mountain landscape', 'duration': 5, 'aspect_ratio': '16:9'}), } class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None opener = urllib.request.build_opener(NoRedirect) def request(path, payload=None, idem=None): headers = {'Authorization': 'Bearer ' + KEY} if idem: headers['Idempotency-Key'] = idem data = None if payload is not None: headers['Content-Type'] = 'application/json' data = json.dumps(payload).encode() req = urllib.request.Request(BASE + '/' + path, data=data, headers=headers) try: return opener.open(req, timeout=300 if payload is not None else 30) except urllib.error.HTTPError as error: # Read the structured failure: an uncertain request may already have a hold. print('HTTP', error.code, 'request_id=', error.headers.get('X-Request-ID'), file=sys.stderr) print(error.read().decode(), file=sys.stderr) raise def stream(response): event = [] def consume(): data = '\n'.join(event) if data == '[DONE]': return True if data: chunk = json.loads(data) if chunk.get('error'): raise RuntimeError(chunk['error']) for choice in chunk.get('choices', []): print(choice.get('delta', {}).get('content') or '', end='', flush=True) if chunk.get('usage'): print('\nusage:', chunk['usage']) return False for raw in response: line = raw.decode('utf-8').rstrip('\r\n') if line == '': if consume(): return event.clear() elif line.startswith('data:'): event.append(line[5:].lstrip(' ')) if event and consume(): return raise RuntimeError('Stream interrupted. Keep request ID and idempotency key; do not create another submission.') def finish(result): deadline = time.monotonic() + 300 while result.get('task_id') and result.get('status') not in ('succeeded', 'failed', 'unknown'): if time.monotonic() >= deadline: print(result) raise TimeoutError('Keep task_id and query later; do not resubmit.') time.sleep(2) with request('tasks/' + result['task_id']) as response: result = json.load(response) print(json.dumps(result, indent=2)) if result.get('status') == 'unknown': raise RuntimeError('Outcome unconfirmed. Retain IDs and wait for reconciliation; do not resubmit.') if result.get('status') == 'failed': raise RuntimeError('Generation failed. Inspect error and billing before a new logical request.') if __name__ == '__main__': path, payload = CASES[MODE] idem = os.environ.get('XXXROUTER_REQUEST_ID') or str(uuid.uuid4()) print('idempotency_key=', idem, file=sys.stderr) # Persist the key with this exact payload before submitting in a real application. with request(path, payload, idem) as response: print('request_id=', response.headers.get('X-Request-ID'), file=sys.stderr) if 'text/event-stream' in response.headers.get('Content-Type', ''): stream(response) else: finish(json.load(response)) ``` ## Related reference [chat](/docs?page=chat&lang=en) · [images](/docs?page=images&lang=en) · [videos](/docs?page=videos&lang=en) · [tasks](/docs?page=tasks&lang=en) · [uploads](/docs?page=uploads&lang=en) · [errors](/docs?page=errors&lang=en) [Runtime documentation](https://docs.python.org/3/library/urllib.request.html) · 2026-10-26 --- # TypeScript / Node.js integration Run these examples on your server. They use the language runtime HTTP client, not a platform SDK. No request is sent until you run a command. Models must be configured and enabled before use. ## Setup Runtime: **Node.js 22.18+**. [Download complete source](/examples/typescript.ts). Set your deployment base URL, including `/v1` exactly once, and a server-side API key. `https://xxxrouter.com` is the official service domain; for local testing, replace it with your deployment origin. The service is not yet deployed and integration certification is not complete. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Run one path `chat` reads SSE deltas and final usage; `image` receives a synchronous image; `async-image` and `video` handle HTTP 202 and query the returned task. Polling is bounded to approximately five minutes plus any in-flight read; a timeout preserves the task for later reads. ```sh node --experimental-strip-types typescript.ts chat # node --experimental-strip-types typescript.ts image # node --experimental-strip-types typescript.ts async-image # node --experimental-strip-types typescript.ts video ``` ## Idempotency and failure handling A new run creates a new idempotency key and can incur a new charge. In an application, persist that key with the exact body before submitting. Set `XXXROUTER_REQUEST_ID` only to resume the same logical request with the same model, payload, API key and source. Do not rerun with a new key after a transport failure. These clients perform one generation POST and never retry it automatically. Record the response `X-Request-ID`. HTTP errors retain the response body for diagnosis. A task with `status=unknown` stops automatic polling and requires later read/reconciliation; it is not a successful or free request. A stream error event or missing `[DONE]` is an interruption. Missing usage is not zero cost. Stopping the connection does not cancel accepted work. Do not log API keys or signed asset URLs. ## Image inputs and result files For image-to-image or image-to-video, pass a public HTTPS URL in `image` when the model supports reference images. The public inference API takes URLs, not file bytes. Playground can upload temporary input images or videos to private R2 storage; only signed-in, verified users can use it. Generated results are never downloaded, stored or rehosted by XXXRouter. Successful responses return the original generated-media URL unchanged. The original host controls availability, expiry and browser cross-origin behavior. Save results before expiry; querying a completed task returns the saved URL and does not renew it. Request metadata and result URLs may be retained for recovery; generated-media bytes are never retained. ## Complete example The code is an integration example, not proof that a live model or payment channel is enabled. Do not place it in a browser bundle. ```typescript // Node.js 22.18+; run: node --experimental-strip-types typescript.ts chat // Uses native fetch, including explicit HTTP and SSE error handling. import { randomUUID } from 'node:crypto'; type Result = { status?: string; task_id?: string; error?: unknown; [key: string]: any }; const base = process.env.XXXROUTER_BASE_URL?.replace(/\/$/, ''); // Include /v1. const key = process.env.XXXROUTER_API_KEY; if (!base || !key) throw new Error('Set XXXROUTER_BASE_URL and XXXROUTER_API_KEY'); const cases: Record]> = { chat: ['chat/completions', { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'Explain HTTP in one sentence.' }], max_tokens: 256, stream: true, stream_options: { include_usage: true } }], image: ['images/generations', { model: 'seedream-4.5', prompt: 'A ceramic cup in natural light', size: '2048x2048', n: 1 }], 'async-image': ['images/generations', { model: 'nano-banana-pro', prompt: 'A ceramic cup in natural light', size: '1024x1024', n: 1 }], video: ['videos/generations', { model: 'kling-3.0', prompt: 'A slow camera move across a mountain landscape', duration: 5, aspect_ratio: '16:9' }], }; async function request(path: string, payload?: unknown, idem?: string): Promise { const headers: Record = { Authorization: `Bearer ${key}` }; if (idem) headers['Idempotency-Key'] = idem; if (payload) headers['Content-Type'] = 'application/json'; const response = await fetch(`${base}/${path}`, { method: payload ? 'POST' : 'GET', headers, redirect: 'error', body: payload ? JSON.stringify(payload) : undefined, signal: AbortSignal.timeout(payload ? 300_000 : 30_000), }); console.error('request_id=', response.headers.get('X-Request-ID')); if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); return response; } async function stream(response: Response): Promise { if (!response.body) throw new Error('Empty response body'); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ''; let data: string[] = []; const consume = (): boolean => { const value = data.join('\n'); data = []; if (value === '[DONE]') return true; if (!value) return false; const chunk = JSON.parse(value); if (chunk.error) throw new Error(JSON.stringify(chunk.error)); for (const choice of chunk.choices ?? []) process.stdout.write(choice.delta?.content ?? ''); if (chunk.usage) console.log('\nusage:', chunk.usage); return false; }; try { while (true) { const { value, done } = await reader.read(); buffer += value ?? ''; let end: number; while ((end = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, end).replace(/\r$/, ''); buffer = buffer.slice(end + 1); if (!line && consume()) return; if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, '')); } if (done) break; } if (buffer.startsWith('data:')) data.push(buffer.slice(5).trimStart()); if (consume()) return; throw new Error('Stream interrupted. Keep request ID and idempotency key; do not resubmit.'); } finally { await reader.cancel(); reader.releaseLock(); } } async function finish(result: Result): Promise { const deadline = Date.now() + 300_000; while (result.task_id && !['succeeded', 'failed', 'unknown'].includes(result.status ?? '')) { if (Date.now() >= deadline) throw new Error(`Query task ${result.task_id} later; do not resubmit`); await new Promise(resolve => setTimeout(resolve, 2000)); result = await (await request(`tasks/${encodeURIComponent(result.task_id)}`)).json(); } console.log(result); if (result.status === 'unknown') throw new Error('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (result.status === 'failed') throw new Error('Generation failed; inspect error and billing.'); } const [path, payload] = cases[process.argv[2] ?? 'chat']; const idem = process.env.XXXROUTER_REQUEST_ID || randomUUID(); console.error('idempotency_key=', idem); // Persist alongside the exact payload before submitting. const response = await request(path, payload, idem); if (response.headers.get('content-type')?.includes('text/event-stream')) await stream(response); else await finish(await response.json()); ``` ## Related reference [chat](/docs?page=chat&lang=en) · [images](/docs?page=images&lang=en) · [videos](/docs?page=videos&lang=en) · [tasks](/docs?page=tasks&lang=en) · [uploads](/docs?page=uploads&lang=en) · [errors](/docs?page=errors&lang=en) [Runtime documentation](https://nodejs.org/api/globals.html#fetch) · 2026-10-26 --- # Go integration Run these examples on your server. They use the language runtime HTTP client, not a platform SDK. No request is sent until you run a command. Models must be configured and enabled before use. ## Setup Runtime: **Go 1.22+**. [Download complete source](/examples/go.go). Set your deployment base URL, including `/v1` exactly once, and a server-side API key. `https://xxxrouter.com` is the official service domain; for local testing, replace it with your deployment origin. The service is not yet deployed and integration certification is not complete. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Run one path `chat` reads SSE deltas and final usage; `image` receives a synchronous image; `async-image` and `video` handle HTTP 202 and query the returned task. Polling is bounded to approximately five minutes plus any in-flight read; a timeout preserves the task for later reads. ```sh go run go.go chat # go run go.go image # go run go.go async-image # go run go.go video ``` ## Idempotency and failure handling A new run creates a new idempotency key and can incur a new charge. In an application, persist that key with the exact body before submitting. Set `XXXROUTER_REQUEST_ID` only to resume the same logical request with the same model, payload, API key and source. Do not rerun with a new key after a transport failure. These clients perform one generation POST and never retry it automatically. Record the response `X-Request-ID`. HTTP errors retain the response body for diagnosis. A task with `status=unknown` stops automatic polling and requires later read/reconciliation; it is not a successful or free request. A stream error event or missing `[DONE]` is an interruption. Missing usage is not zero cost. Stopping the connection does not cancel accepted work. Do not log API keys or signed asset URLs. ## Image inputs and result files For image-to-image or image-to-video, pass a public HTTPS URL in `image` when the model supports reference images. The public inference API takes URLs, not file bytes. Playground can upload temporary input images or videos to private R2 storage; only signed-in, verified users can use it. Generated results are never downloaded, stored or rehosted by XXXRouter. Successful responses return the original generated-media URL unchanged. The original host controls availability, expiry and browser cross-origin behavior. Save results before expiry; querying a completed task returns the saved URL and does not renew it. Request metadata and result URLs may be retained for recovery; generated-media bytes are never retained. ## Complete example The code is an integration example, not proof that a live model or payment channel is enabled. Do not place it in a browser bundle. ```go // Go 1.22+, standard library only. Run: go run go.go chat package main import ( "bufio" "bytes" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) type Object = map[string]any var base = strings.TrimRight(os.Getenv("XXXROUTER_BASE_URL"), "/") // Include /v1. var key = os.Getenv("XXXROUTER_API_KEY") var client = &http.Client{Timeout: 300 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} func request(path string, payload Object, idem string) (*http.Response, error) { var body io.Reader method := "GET" if payload != nil { raw, err := json.Marshal(payload) if err != nil { return nil, err } body = bytes.NewReader(raw) method = "POST" } req, err := http.NewRequest(method, base+"/"+path, body) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) if payload != nil { req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", idem) } response, err := client.Do(req) if err != nil { return nil, err } fmt.Fprintln(os.Stderr, "request_id=", response.Header.Get("X-Request-ID")) if response.StatusCode < 200 || response.StatusCode >= 300 { defer response.Body.Close() raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20)) return nil, fmt.Errorf("HTTP %d: %s", response.StatusCode, raw) } return response, nil } func decode(response *http.Response) (Object, error) { defer response.Body.Close() var result Object err := json.NewDecoder(response.Body).Decode(&result) return result, err } func stream(response *http.Response) error { defer response.Body.Close() scanner := bufio.NewScanner(response.Body) scanner.Buffer(make([]byte, 4096), 2<<20) var parts []string consume := func() (bool, error) { data := strings.Join(parts, "\n") parts = nil if data == "[DONE]" { return true, nil } if data == "" { return false, nil } var chunk struct { Error json.RawMessage `json:"error"` Usage json.RawMessage `json:"usage"` Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` } if err := json.Unmarshal([]byte(data), &chunk); err != nil { return false, err } if len(chunk.Error) > 0 && string(chunk.Error) != "null" { return false, fmt.Errorf("stream error: %s", chunk.Error) } for _, choice := range chunk.Choices { fmt.Print(choice.Delta.Content) } if len(chunk.Usage) > 0 && string(chunk.Usage) != "null" { fmt.Printf("\nusage: %s\n", chunk.Usage) } return false, nil } for scanner.Scan() { line := scanner.Text() if line == "" { done, err := consume() if err != nil || done { return err } } if strings.HasPrefix(line, "data:") { parts = append(parts, strings.TrimPrefix(line[5:], " ")) } } if err := scanner.Err(); err != nil { return err } done, err := consume() if err != nil || done { return err } return fmt.Errorf("stream interrupted; keep request ID and idempotency key, do not resubmit") } func finish(result Object) error { deadline := time.Now().Add(5 * time.Minute) for { task, hasTask := result["task_id"].(string) status, _ := result["status"].(string) if !hasTask || status == "succeeded" || status == "failed" || status == "unknown" { break } if time.Now().After(deadline) { return fmt.Errorf("query task %s later; do not resubmit", task) } time.Sleep(2 * time.Second) response, err := request("tasks/"+task, nil, "") if err != nil { return err } result, err = decode(response) if err != nil { return err } } raw, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(raw)) if result["status"] == "unknown" { return fmt.Errorf("outcome unconfirmed; retain IDs and wait for reconciliation") } if result["status"] == "failed" { return fmt.Errorf("generation failed; inspect error and billing") } return nil } func run() error { if base == "" || key == "" { return fmt.Errorf("set XXXROUTER_BASE_URL and XXXROUTER_API_KEY") } mode := "chat" if len(os.Args) > 1 { mode = os.Args[1] } path := "images/generations" var payload Object switch mode { case "chat": path = "chat/completions" payload = Object{"model": "deepseek-v4-flash", "messages": []Object{{"role": "user", "content": "Explain HTTP in one sentence."}}, "max_tokens": 256, "stream": true, "stream_options": Object{"include_usage": true}} case "image": payload = Object{"model": "seedream-4.5", "prompt": "A ceramic cup in natural light", "size": "2048x2048", "n": 1} case "async-image": payload = Object{"model": "nano-banana-pro", "prompt": "A ceramic cup in natural light", "size": "1024x1024", "n": 1} case "video": path = "videos/generations" payload = Object{"model": "kling-3.0", "prompt": "A slow camera move across a mountain landscape", "duration": 5, "aspect_ratio": "16:9"} default: return fmt.Errorf("choose chat, image, async-image or video") } idem := os.Getenv("XXXROUTER_REQUEST_ID") if idem == "" { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return err } idem = hex.EncodeToString(b) } fmt.Fprintln(os.Stderr, "idempotency_key=", idem) // Persist with the exact payload before submission. response, err := request(path, payload, idem) if err != nil { return err } if strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { return stream(response) } result, err := decode(response) if err != nil { return err } return finish(result) } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } ``` ## Related reference [chat](/docs?page=chat&lang=en) · [images](/docs?page=images&lang=en) · [videos](/docs?page=videos&lang=en) · [tasks](/docs?page=tasks&lang=en) · [uploads](/docs?page=uploads&lang=en) · [errors](/docs?page=errors&lang=en) [Runtime documentation](https://pkg.go.dev/net/http) · 2026-10-26 --- # PHP integration Run these examples on your server. They use the language runtime HTTP client, not a platform SDK. No request is sent until you run a command. Models must be configured and enabled before use. ## Setup Runtime: **PHP 8.1+ + ext-curl**. [Download complete source](/examples/php.php). Set your deployment base URL, including `/v1` exactly once, and a server-side API key. `https://xxxrouter.com` is the official service domain; for local testing, replace it with your deployment origin. The service is not yet deployed and integration certification is not complete. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Run one path `chat` reads SSE deltas and final usage; `image` receives a synchronous image; `async-image` and `video` handle HTTP 202 and query the returned task. Polling is bounded to approximately five minutes plus any in-flight read; a timeout preserves the task for later reads. ```sh php php.php chat # php php.php image # php php.php async-image # php php.php video ``` ## Idempotency and failure handling A new run creates a new idempotency key and can incur a new charge. In an application, persist that key with the exact body before submitting. Set `XXXROUTER_REQUEST_ID` only to resume the same logical request with the same model, payload, API key and source. Do not rerun with a new key after a transport failure. These clients perform one generation POST and never retry it automatically. Record the response `X-Request-ID`. HTTP errors retain the response body for diagnosis. A task with `status=unknown` stops automatic polling and requires later read/reconciliation; it is not a successful or free request. A stream error event or missing `[DONE]` is an interruption. Missing usage is not zero cost. Stopping the connection does not cancel accepted work. Do not log API keys or signed asset URLs. ## Image inputs and result files For image-to-image or image-to-video, pass a public HTTPS URL in `image` when the model supports reference images. The public inference API takes URLs, not file bytes. Playground can upload temporary input images or videos to private R2 storage; only signed-in, verified users can use it. Generated results are never downloaded, stored or rehosted by XXXRouter. Successful responses return the original generated-media URL unchanged. The original host controls availability, expiry and browser cross-origin behavior. Save results before expiry; querying a completed task returns the saved URL and does not renew it. Request metadata and result URLs may be retained for recovery; generated-media bytes are never retained. ## Complete example The code is an integration example, not proof that a live model or payment channel is enabled. Do not place it in a browser bundle. ```php getMessage(); } }; $curl = curl_init($base . '/' . $path); $options = [ CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $payload !== null ? 300 : 30, CURLOPT_HEADERFUNCTION => function ($handle, string $line) use (&$contentType, &$requestId): int { if (stripos($line, 'Content-Type:') === 0) $contentType = trim(substr($line, 13)); if (stripos($line, 'X-Request-ID:') === 0) $requestId = trim(substr($line, 13)); return strlen($line); }, CURLOPT_WRITEFUNCTION => function ($handle, string $bytes) use (&$contentType, &$raw, &$buffer, &$data, &$done, &$streamError, $consume): int { if (!str_contains($contentType, 'text/event-stream')) { $raw .= $bytes; return strlen($bytes); } $buffer .= $bytes; while (($end = strpos($buffer, "\n")) !== false) { $line = rtrim(substr($buffer, 0, $end), "\r"); $buffer = substr($buffer, $end + 1); if ($line === '') $consume(); elseif (str_starts_with($line, 'data:')) $data[] = ltrim(substr($line, 5), ' '); } return strlen($bytes); }, ]; if ($payload !== null) { $headers[] = 'Content-Type: application/json'; $headers[] = 'Idempotency-Key: ' . $idem; $options[CURLOPT_POST] = true; $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_THROW_ON_ERROR); } $options[CURLOPT_HTTPHEADER] = $headers; curl_setopt_array($curl, $options); try { $ok = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); fwrite(STDERR, 'request_id=' . $requestId . "\n"); if ($ok === false) throw new RuntimeException('Transport error; retain IDs, do not resubmit: ' . curl_error($curl)); if ($status < 200 || $status >= 300) throw new RuntimeException('HTTP ' . $status . ': ' . $raw); if (str_contains($contentType, 'text/event-stream')) { if (str_starts_with($buffer, 'data:')) $data[] = ltrim(substr($buffer, 5), ' '); $consume(); if ($streamError !== null) throw new RuntimeException($streamError); if (!$done) throw new RuntimeException('Stream interrupted; retain request ID and idempotency key.'); return ['stream_complete' => true]; } return json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } finally { curl_close($curl); } } $cases = [ 'chat' => ['chat/completions', ['model'=>'deepseek-v4-flash','messages'=>[['role'=>'user','content'=>'Explain HTTP in one sentence.']],'max_tokens'=>256,'stream'=>true,'stream_options'=>['include_usage'=>true]]], 'image' => ['images/generations', ['model'=>'seedream-4.5','prompt'=>'A ceramic cup in natural light','size'=>'2048x2048','n'=>1]], 'async-image' => ['images/generations', ['model'=>'nano-banana-pro','prompt'=>'A ceramic cup in natural light','size'=>'1024x1024','n'=>1]], 'video' => ['videos/generations', ['model'=>'kling-3.0','prompt'=>'A slow camera move across a mountain landscape','duration'=>5,'aspect_ratio'=>'16:9']], ]; $mode = $argv[1] ?? 'chat'; if (!isset($cases[$mode])) throw new InvalidArgumentException('Choose chat, image, async-image or video'); [$path, $payload] = $cases[$mode]; $idem = getenv('XXXROUTER_REQUEST_ID') ?: bin2hex(random_bytes(16)); fwrite(STDERR, 'idempotency_key=' . $idem . "\n"); // Persist alongside exact payload before submission. $result = request($path, $payload, $idem); $deadline = microtime(true) + 300; while (isset($result['task_id']) && !in_array($result['status'], ['succeeded', 'failed', 'unknown'], true)) { if (microtime(true) >= $deadline) throw new RuntimeException('Query task ' . $result['task_id'] . ' later; do not resubmit.'); sleep(2); $result = request('tasks/' . rawurlencode($result['task_id'])); } echo json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . "\n"; if (($result['status'] ?? '') === 'unknown') throw new RuntimeException('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (($result['status'] ?? '') === 'failed') throw new RuntimeException('Generation failed; inspect error and billing.'); ``` ## Related reference [chat](/docs?page=chat&lang=en) · [images](/docs?page=images&lang=en) · [videos](/docs?page=videos&lang=en) · [tasks](/docs?page=tasks&lang=en) · [uploads](/docs?page=uploads&lang=en) · [errors](/docs?page=errors&lang=en) [Runtime documentation](https://www.php.net/manual/en/function.curl-exec.php) · 2026-10-26 --- # Changelog ## 0.1.0 — 2026-10-26 — Implementation draft - Added USD account balance and per-key spending controls. - Added en / zh-CN developer reference and machine-readable OpenAPI draft. - Distinguished direct synchronous image results from asynchronous image/video tasks. - Defined OpenAI-style text chat with optional SSE. - Added $0.20 trial credit with a 7-day expiry and Payblis / NOWPayments hosted checkouts. - Set the default request-log policy to metadata only, 30 days. This reference describes implemented endpoints. Deployment, live route certification and payment-channel acceptance remain separate release requirements. [Quickstart](/docs?page=quickstart&lang=en) --- # 快速开始 > 实施参考 · 2026-10-26 · 完成部署配置后再发送真实请求。 应用使用服务端身份、余额和请求状态。`https://xxxrouter.com` 是正式服务域名;本地测试请替换为你的部署 origin。当前服务尚未部署,联调认证尚未完成;支持同源 `/v1` 网关访问。模型初始禁用,运营配置凭证、认证路由及报价后才可启用。本文不代表部署或集成验收已经完成。 1. 登录控制台,查看美元余额。 2. 创建 API Key;仅创建时显示完整密钥,复制到服务端密钥管理系统。 3. 查看模型的响应模式与生效价格。 4. 从自己的后端调用,在用量日志中用请求 ID 追踪结果。 ```sh export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' curl https://xxxrouter.com/v1/models \ -H "Authorization: Bearer $XXXROUTER_API_KEY" ``` ```sh curl https://xxxrouter.com/v1/chat/completions \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-chat-request-001' \ -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"你好"}],"max_tokens":256,"stream":false}' ``` 对话接口采用 OpenAI 风格契约。图片接口兼有直接结果和异步任务,不能宣称所有模型都可直接替换 OpenAI Images。Playground 与外部调用遵循相同定价和账务规则,通过登录会话调用,并与 API Key 分开统计归属。 继续阅读:[身份验证](/docs?page=auth&lang=zh-CN)、[模型能力](/docs?page=models&lang=zh-CN)、[图片生成](/docs?page=images&lang=zh-CN)、[错误处理](/docs?page=errors&lang=zh-CN)。机器可读入口:[OpenAPI](/openapi.json)、[llms.txt](/llms.txt)、[完整文本](/llms-full.txt)。 --- # 身份验证 生成、任务及 Key 请求携带 `Authorization: Bearer `;模型目录 GET 可匿名读取公开价格,携带有效 Key 时返回账户价格。不要把密钥写入 URL、公共仓库、前端构建包、共享截图或浏览器存储。请在已登录且完成邮箱验证的控制台账户中创建密钥。 正式密钥仅在创建时完整展示一次;后续只显示前后缀和不可变 ID。服务端仅保存不可逆摘要。管理密钥需要控制台会话;推理密钥不能创建其他密钥或修改账户账单。 轮换流程:新建替代密钥、部署替换、确认流量、吊销旧密钥。停用可恢复,吊销永久生效;到期时刻按 UTC 判断。停用、过期、吊销后已接受的任务仍继续完成和记账;查询结果改用同账户其他有效密钥或控制台。 `GET /v1/key` 仅返回当前密钥的元数据、生效限制和剩余额度,不返回密钥明文。认证错误不得泄露被猜测的密钥属于哪个账户。 [密钥限制](/docs?page=keys&lang=zh-CN) · [错误处理](/docs?page=errors&lang=zh-CN) --- # 模型与价格 `GET /v1/models` 返回当前账户可用模型;`GET /v1/models/{id}` 返回能力、参数、响应模式、可用性及生效价格。公共模型 ID 使用平台命名,不含路由前缀,例如 `/v1/models/seedream-4.5`。路由和基础设施元数据属于内部信息,不通过公共模型 API 返回。 | 演示模型 ID | 输出 | 响应模式 | |---|---|---| | `seedream-4.5` | 图片 | `sync` | | `nano-banana-pro` | 图片 | `async` | | `flux-2-pro` | 图片 | `async` | | `kling-3.0` | 视频 | `async` | | `deepseek-v4-flash` | 文本 | `sync`,`stream=true` 时 SSE | 这些是平台公共模型 ID,不代表已配置并认证的上线能力。响应模式由模型目录显式声明,不能通过 URL 是否带 async 或代码是否使用 `async def` 推断。模型与参数在预占余额和供应商提交之前验证。 价格统一以 USD 表示。图片依照公开的参数维度计价,本版本视频按时长计价,LLM 输入与输出 token 分别计价。请求接受时保存生效价格版本;后续调价不影响该请求。预估费用不等于结算费用。 [图片协议](/docs?page=images&lang=zh-CN) · [余额与计费](/docs?page=billing&lang=zh-CN) ## 生效价格优先级 只选择一条规则:用户 × 模型专属价 > 价格组 × 模型价 > 价格组默认折扣 > 公开价,不叠加。专属价可为固定价或折扣,折扣受模型允许范围约束;低于成本必须由运营明确确认,成本未知不展示虚构毛利。请求接受时保存规则、单位和版本快照。 模型 `parameters` 列表声明平台统一顶层字段:图片为 `size`、`n`、`image`;视频为 `duration`、`aspect_ratio`、`image`;对话为 `temperature`、`max_tokens`、`stream`、`stream_options`,不是任意参数透传字典。Seedream 4.5 示例使用 `2048x2048`,正式参数限制必须逐模型验证。UI 示例选项不代表真实可用能力承诺。 模型 JSON 使用 name、modality(llm/image/video)、mode/response_mode(stream/sync/async)、parameters、sizes、supportsImage、enabled。pricing 是含 currency、price 与可空 outputPrice 的对象(金額为六位小数字符串),顶层 price/outputPrice 是 USD 数字。禁用模型不可调用。初始报价待运营确认,未知成本不展示毛利。 --- # 对话补全 `POST /v1/chat/completions` 接受 `model`、`messages`、`max_tokens`、`temperature`、`stream` 和可选的 `stream_options: {"include_usage": true}`。v0.1 契约覆盖 `system`、`user`、`assistant` 文本消息;工具调用、多模态对话、embeddings 和 Responses API 不在本轮契约内。 ```json {"model":"deepseek-v4-flash","messages":[{"role":"user","content":"用一句话解释 HTTP。"}],"max_tokens":256,"temperature":0.7,"stream":true,"stream_options":{"include_usage":true}} ``` 非流式结果包含 `id`、`object: "chat.completion"`、`model`、`choices`、`usage`。流式返回 `text/event-stream`,每个 data 事件通过 `choices[].delta` 增量返回内容;设置 `stream_options.include_usage=true` 后,上游返回的最终用量事件位于 `data: [DONE]` 前。通过响应头 `X-Request-ID` 追踪请求,不能把 SSE 当作一整份 JSON 解析。 网关不能强制插入 JSON 提示、静默换模型或为修复内容重复购买调用。断开连接或点击停止不会清除已生成 token 的费用。根据实际用量结算;缺少权威用量时进入待对账,不能默认记零。每条路由启用前必须完成真实集成认证。 [余额与计费](/docs?page=billing&lang=zh-CN) · [生产接入](/docs?page=production&lang=zh-CN) 全部生成 POST 必须携带 Idempotency-Key(1–128 个可打印 ASCII 字符)。新逻辑请求使用新值,原请求重试仅复用完全相同请求体的键。max_tokens 为 1–4096,temperature 为 0–2;流式重放不会重新购买或回放已付费上游流。 --- # 图片生成 `POST /v1/images/generations` 有两种明确的响应模式,由模型目录指定。先查看模型元数据,再处理对应响应。两种模式均接受 `model`、`prompt`、可选的顶层参考图 URL `image`、`size` 和 `n`。这些是平台统一字段,供应商适配器再转换为已认证的模型 schema。 ```sh curl https://xxxrouter.com/v1/images/generations \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-image-request-001' \ -d '{"model":"seedream-4.5","prompt":"自然光下的一只陶瓷杯","size":"2048x2048","n":1}' ``` ## 同步模型:HTTP 200 ```json {"request_id":"req_demo_sync","model":"seedream-4.5","response_mode":"sync","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/cup.png"}],"billing":{"status":"settled","currency":"USD","amount":"0.030000"}} ``` 图片在原 HTTP 响应中直接返回,无供应商任务 ID,无需任务轮询。上例金额仅为演示,不是报价。同步请求耗时较长仍然是同步;客户端超时不能证明生成失败,也不能据此重新付费提交。 ## 异步模型:HTTP 202 选择 `nano-banana-pro` 或 `flux-2-pro`,得到: ```json {"request_id":"req_demo_async","task_id":"task_demo_01","model":"nano-banana-pro","response_mode":"async","status":"running"} ``` 通过[任务接口](/docs?page=tasks&lang=zh-CN)查询直到终态。202 代表接受任务,并不代表生成完成。统一图片入口不代表所有模型与 OpenAI Images 完全兼容。已配置路由提交真实请求;缺少凭证时不能返回模拟付费结果。 [参考素材](/docs?page=uploads&lang=zh-CN) · [错误处理](/docs?page=errors&lang=zh-CN) 当前服务仅支持 n=1;只有模型 parameters 声明 image 时才接受参考图。 成功响应原样返回生成结果的原始 URL。链接可用性、有效期和浏览器跨域行为由原始主机决定。请在到期前保存结果;查询已完成任务只返回已记录的 URL,不会续期。本站可为请求恢复保留元数据和结果 URL,但不保留生成结果文件。 --- # 视频生成 `POST /v1/videos/generations` 返回 HTTP 202 和任务。公共模型 ID 为 `kling-3.0`;实际可用性仍取决于路由配置和认证,目录选项不能被视作已上线能力。 ```json {"model":"kling-3.0","prompt":"镜头缓慢环绕一只陶瓷杯","duration":5,"aspect_ratio":"16:9"} ``` 每次逻辑提交携带 `Idempotency-Key`,保存 `task_id` 并查询 `/v1/tasks/{task_id}`。成功结果包含视频 URL、媒体类型和结算信息。供应商未支持时,不承诺真实取消能力或精确百分比;没有可靠上游依据时不展示精确进度。 请求时长是报价输入,不保证生成结果精确达到该时长。上线前应逐路由明确实际时长和参数组合的计费方式,不默认跨模型自动回退。 [任务](/docs?page=tasks&lang=zh-CN) · [余额与计费](/docs?page=billing&lang=zh-CN) 统一顶层参数为 `duration`、`aspect_ratio` 和可选 `image`;当前 schema 接受 5/10 秒与 16:9、9:16 或 1:1;调用前核对模型可用性。当前请求草案不包含 `resolution` 或嵌套 `parameters` 字段。 成功响应原样返回生成结果的原始 URL。链接可用性、有效期和浏览器跨域行为由原始主机决定。请在到期前保存结果;查询已完成任务只返回已记录的 URL,不会续期。本站可为请求恢复保留元数据和结果 URL,但不保留生成结果文件。 --- # 任务与幂等 `GET /v1/tasks/{id}` 查询已接受的图片/视频任务,每次读取均验证身份和账户归属。同账户其他有效密钥可查询任务,原始 key ID 始终作为不可变的用量归属。 状态:`submitting → running → succeeded | failed`。结果不明时请求 `status` 为 `unknown`,`billing.status` 为 `reconciling`,禁止自动重发。结果状态与账务状态分开:结果已经可用时,费用仍可能待对账。 ```json {"task_id":"task_demo_01","request_id":"req_demo_async","model":"nano-banana-pro","response_mode":"async","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/image.png","media_type":"image"}],"billing":{"status":"settled","currency":"USD","amount":"0.150000"}} ``` 有界轮询建议从 2 秒开始,退避到最多 10 秒,遵循 `Retry-After`。重试 GET 不创建新工作。POST 响应丢失时,复用同一幂等键与完全相同的请求体:同账户、接口、幂等键、请求指纹返回原记录,变更请求体则返回 `409 IDEMPOTENCY_CONFLICT`。幂等记录持久化保存在 MySQL,不能认为跨日后即可复用旧键或盲目重试结果不明的请求。 同步图片的幂等重放在结果可用时返回原直接结果;请求仍执行或结果不明时返回 HTTP `409` 及当前请求记录,不能再次提交上游。同步请求 ID 不能冒充 task ID 查询 `/tasks`。 [错误处理](/docs?page=errors&lang=zh-CN) · [生产接入](/docs?page=production&lang=zh-CN) 成功响应原样返回生成结果的原始 URL。链接可用性、有效期和浏览器跨域行为由原始主机决定。请在到期前保存结果;查询已完成任务只返回已记录的 URL,不会续期。本站可为请求恢复保留元数据和结果 URL,但不保留生成结果文件。 --- # 余额与计费 账户共用一个预付 USD 余额。Key 消费限额限制支出,不是独立钱包。明确区分可用余额、预占金额和已结算金额;使用整数微美元或精确十进制存储,不能使用二进制浮点数结算。 已确认试用赠金为 $0.20,有效期 7 天。赠金与付费资金分开,不可提现;发放资格、唯一领取及到期均由服务端处理;先验证邮箱并完成验证码,付费资金与赠金分开。 接受请求前,原子检查并预占账户资金和 Key 额度。完成后根据实际金额结算并释放差额;没有可计费使用量的失败释放预占。供应商结果不明进入待对账,不能创建第二笔调用。超时或中断流不意味着零费用。 支付只包含 Payblis 与 NOWPayments。配置后的渠道创建托管收银台订单;缺少凭证时显示不可用。浏览器成功跳转不能直接增加余额;验签回调和主动对账必须校验币种、金额、渠道流水及订单身份,之后唯一入账。 已实现订单状态为 `creating`、`pending`、`confirming`、`paid`、`underpaid`、`expired`、`failed`。少付保持 `underpaid`,直至确认足额到账。本版尚未提供退款 API 或自动退款流程;未来退款必须追加补偿账本,不得覆盖历史交易。 [密钥限制](/docs?page=keys&lang=zh-CN) · [生产接入](/docs?page=production&lang=zh-CN) ## 领取条件与扣款顺序 邮箱验证并完成验证码挑战后才可领取 $0.20,领取幂等并受频控。自发放时刻起满 7 天到期,优先消耗尚有效的赠金,再消耗付费余额;试用与付费请求适用相同生效价格和账户/Key 限制。预占保存资金来源;赠金到期后释放该部分预占,归入已过期赠金,不能变成可消费付费余额。赠金过期不影响付费资金。财务账本永久保留,与 30 天请求日志独立。 --- # API Key 管理 通过控制台管理密钥;`GET /v1/key` 是只读的当前密钥查询接口。每个 Key 拥有不可变 ID、名称、脱敏密钥、状态、创建时间、可选过期时间、消费限额和独立用量归属。 账户余额与 Key 剩余额度必须同时覆盖新请求;预占金额同时占用两者,避免并发调用读取旧余额后穿透上限。Key 无限额仍受账户余额约束。将限额调低至当前占用以下会阻止新请求,不清空已消费金额,不取消已接受任务。 停用拒绝新调用,可恢复;吊销永久拒绝新调用。账户有钱也不能使用已过期 Key。已接受任务继续执行,并保留原 Key ID;替代密钥形成独立的用量历史。 Playground 通过登录会话认证,以 `playground` 来源单列,不暴露或要求用户长期密钥。账户图表包含 Playground 和外部 API;分 Key 图表仅统计对应 Key,归档或吊销 Key 仍可作为历史筛选条件。 服务端仅创建时返回一次明文,后续查询不可恢复。 [身份验证](/docs?page=auth&lang=zh-CN) · [余额与计费](/docs?page=billing&lang=zh-CN) ## 限制字段 每个 Key 设置一个消费上限,并选择一种周期:累计、日、周或月。可编辑周期;按当前窗口重算已消费,不删除历史。日、周、月分别在 UTC 每日零点、周一零点、每月 1 日零点重置。全部未结算预占计入当前上限,包括跨周期的在途预占,避免重置导致绕过上限;结算后的消费按请求接受时间归属,累计周期包含全部历史。模型 allowlist、RPM、IPv4/IPv6 地址与 CIDR 白名单分别增加准入条件。空模型/IP 白名单表示不限制,界面必须明确标注;支持 IPv4 与 IPv6。账户平台上限仍然生效。Key 剩余额度不等于账户余额。限制由服务端校验,生产必须正确配置可信代理和真实客户端 IP。 GET /v1/key 返回 id、name、mask、enabled、revoked、limit、period、expiresAt、models、rpm、ips、spent、reserved、nextReset。Key 金额字段使用 USD 数字;limit=null 表示 Key 无消费上限;limit=0 不允许正数消费。两者仍受账户余额约束。不会返回密钥明文。 --- # 错误与限制 ```json {"error":{"code":"KEY_BUDGET_EXCEEDED","message":"当前 Key 剩余消费额度不足。","request_id":"req_demo_error"}} ``` | HTTP | 错误码 | 处理方式 | |---|---|---| | 422 | `INVALID_INPUT` | 修改输入后提交。 | | 401 | `INVALID_API_KEY`、`API_KEY_INACTIVE` | 使用有效凭据。 | | 403 | `MODEL_NOT_ALLOWED`、`IP_NOT_ALLOWED` | 在控制台核对密钥限制。 | | 402 | `INSUFFICIENT_BALANCE`、`KEY_BUDGET_EXCEEDED` | 充值或调整获授权的消费限额。 | | 404 | `MODEL_NOT_FOUND`、`REQUEST_NOT_FOUND` | 核对 ID 与账户归属。 | | 409 | `IDEMPOTENCY_CONFLICT` | 恢复原请求体,或创建新的逻辑请求。 | | 409 | 执行中请求记录或 `RESPONSE_CACHE_EXPIRED` | 等待/对账,不能重复购买生成。 | | 422 | `UNSUPPORTED_PARAMETER` | 修正模型/输入选择。 | | 429 | `RATE_LIMITED` | 遵循 Retry-After,降低流量。 | | 502/503 | `MODEL_UNAVAILABLE` | 重试提交前先确认原请求状态。 | 错误信息经过清洗,不能展示供应商余额、商户密钥和原始堆栈。使用 `X-Request-ID` 排查。未知供应商错误不能误标为内容政策问题。SSE 响应头发出后发生错误,应发送流内错误事件并关闭连接,不能再发送第二个 HTTP 响应。 [幂等](/docs?page=tasks&lang=zh-CN) · [生产接入](/docs?page=production&lang=zh-CN) --- # 生产接入 本文配套真实服务实现,部署与集成认证仍需单独完成。真实上线前应在独立测试环境认证每条供应商路由、参数 schema、计价方式、流式用量、支付签名和故障恢复流程。 - 使用服务端密钥、TLS、明确超时、有界轮询和请求 ID。 - 提交前持久化幂等记录;计费 POST 出现结果不明的传输错误后,禁止盲目重试。 - 钱包账本、预占、Key 预算、支付入账由 MySQL 事务负责;DynamoDB 存请求日志事件及查询投影及默认保留 30 天的请求日志,不承担另一套钱包真源。 - 持久化预占与提交 claim 后直接转发;拿到上游任务 ID 后返回平台任务。GET 查询并结算,定时扫描补偿无人查询任务与日志写入失败。 - 默认日志仅存元数据,30 天过期;不记录提示词、对话正文、输入文件、生成文件、Authorization 请求头或支付密钥。 - 账本/支付记录具有独立保留政策,删除请求元数据不删除财务历史;生产保留要求上线前另行确定。 - 监控待对账积压、卡住的预占、重复入账尝试、任务耗时、日志重试积压、供应商错误和支付金额不符。 上线前配置凭证、DNS/TLS、工作负载权限,并完成真实集成验收。 [任务](/docs?page=tasks&lang=zh-CN) · [余额与计费](/docs?page=billing&lang=zh-CN) MySQL 负责任务协调、账务事务与日志重试记录。元数据直接写 DynamoDB,按账户/Key/时间查询,采用日期桶和 LOG_SHARDS 分片;查询主动过滤过期记录。轻量扫描进程补偿无人查询任务并重试日志写入。web、gateway、reconciler 独立部署. 成功响应原样返回生成结果的原始 URL。链接可用性、有效期和浏览器跨域行为由原始主机决定。请在到期前保存结果;查询已完成任务只返回已记录的 URL,不会续期。本站可为请求恢复保留元数据和结果 URL,但不保留生成结果文件。 --- # 参考 URL 与临时素材 模型元数据支持 `image` 时,可在图片或视频请求中传入一个顶层 HTTPS 参考图 URL。浏览器 `blob:` URL 不能被服务端读取。当前统一请求不支持多张参考图。 模型支持参考图时,通过 `image` 传入公开可访问的 HTTPS 地址。公共推理 API 接收 URL,不接收文件字节。Playground 可将临时输入图片或视频上传到私有 R2,仅登录且已验证的账户可用。XXXRouter 不下载、保存或转存生成结果。 临时素材支持不超过 10 MiB 的 PNG/JPEG/WebP 图片和不超过 50 MiB 的 MP4/WebM 视频,每账户每分钟最多尝试上传 20 次。读取链接最长有效 1 小时,不提供续签;对象按 1 天到期规则异步清理,不保证到第 24 小时瞬间物理删除。图片 URL 可自动填入支持的参考图字段;视频 URL 仅可用于明确支持视频输入的模型,不能作为参考图传入。使用外部素材地址时,应保证模型处理期间可访问。 ```json {"model":"seedream-4.5","prompt":"A small blue house","image":"https://example.com/reference.png","n":1,"size":"2048x2048"} ``` 成功响应原样返回生成结果的原始 URL。链接可用性、有效期和浏览器跨域行为由原始主机决定。请在到期前保存结果;查询已完成任务只返回已记录的 URL,不会续期。本站可为请求恢复保留元数据和结果 URL,但不保留生成结果文件。 --- # Python 接入 请在服务端运行示例,使用语言自带 HTTP 客户端,不依赖平台 SDK。只有执行运行命令才会发送请求;模型须先完成配置并启用。 ## 环境准备 运行环境: **Python 3.11+**. [下载完整源码](/examples/python.py). 设置部署地址,末尾仅包含一次 `/v1`,并通过环境变量提供服务端 API Key。`https://xxxrouter.com` 是正式服务域名;本地测试请替换为你的部署 origin。当前尚未部署,联调认证尚未完成。 ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 选择一种调用方式 `chat` 读取 SSE 增量内容和最终用量;`image` 接收同步图片;`async-image` 和 `video` 处理 HTTP 202 并查询返回的任务。轮询约五分钟后停止(另加正在进行的读请求时间);超时后保留任务 ID,稍后继续查询。 ```sh python3 python.py chat # python3 python.py image # python3 python.py async-image # python3 python.py video ``` ## 幂等与异常恢复 每次重新运行默认创建新的幂等键,可能产生一笔新费用。应用应在提交前持久化幂等键和完整请求体。仅在恢复同一个逻辑请求、且模型、请求体、API Key 和来源完全一致时,设置 `XXXROUTER_REQUEST_ID` 为原值;网络错误后不要换新键重跑。这些客户端只发起一次生成 POST,不会自动重试生成。 保存响应头 `X-Request-ID`。HTTP 错误会保留响应正文供排查。任务 `status=unknown` 时停止自动轮询,稍后读取或等待对账,不视为成功或免费调用。流中的错误事件或缺少 `[DONE]` 均表示中断;没有用量不等于零费用。断开连接不取消已接受的任务。不要记录 API Key 或签名媒体 URL。 ## 参考图片与结果文件 模型支持参考图时,通过 `image` 传入公开可访问的 HTTPS 地址。公共推理 API 接收 URL,不接收文件字节。Playground 可将临时输入图片或视频上传到私有 R2,仅登录且已验证的账户可用。XXXRouter 不下载、保存或转存生成结果。 成功响应原样返回生成结果的原始 URL。链接可用性、有效期和浏览器跨域行为由原始主机决定。请在到期前保存结果;查询已完成任务只返回已记录的 URL,不会续期。本站可为请求恢复保留元数据和结果 URL,但不保留生成结果文件。 ## 完整示例 这些代码是接入示例,不代表真实模型或支付渠道已启用。请勿打包进浏览器端。 ```python """Python 3.11+, standard library only. No automatic generation retries.""" import json import os import sys import time import urllib.error import urllib.request import uuid BASE = os.environ['XXXROUTER_BASE_URL'].rstrip('/') # Include /v1 exactly once. KEY = os.environ['XXXROUTER_API_KEY'] MODE = sys.argv[1] if len(sys.argv) > 1 else 'chat' CASES = { 'chat': ('chat/completions', {'model': 'deepseek-v4-flash', 'messages': [{'role': 'user', 'content': 'Explain HTTP in one sentence.'}], 'max_tokens': 256, 'stream': True, 'stream_options': {'include_usage': True}}), 'image': ('images/generations', {'model': 'seedream-4.5', 'prompt': 'A ceramic cup in natural light', 'size': '2048x2048', 'n': 1}), 'async-image': ('images/generations', {'model': 'nano-banana-pro', 'prompt': 'A ceramic cup in natural light', 'size': '1024x1024', 'n': 1}), 'video': ('videos/generations', {'model': 'kling-3.0', 'prompt': 'A slow camera move across a mountain landscape', 'duration': 5, 'aspect_ratio': '16:9'}), } class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None opener = urllib.request.build_opener(NoRedirect) def request(path, payload=None, idem=None): headers = {'Authorization': 'Bearer ' + KEY} if idem: headers['Idempotency-Key'] = idem data = None if payload is not None: headers['Content-Type'] = 'application/json' data = json.dumps(payload).encode() req = urllib.request.Request(BASE + '/' + path, data=data, headers=headers) try: return opener.open(req, timeout=300 if payload is not None else 30) except urllib.error.HTTPError as error: # Read the structured failure: an uncertain request may already have a hold. print('HTTP', error.code, 'request_id=', error.headers.get('X-Request-ID'), file=sys.stderr) print(error.read().decode(), file=sys.stderr) raise def stream(response): event = [] def consume(): data = '\n'.join(event) if data == '[DONE]': return True if data: chunk = json.loads(data) if chunk.get('error'): raise RuntimeError(chunk['error']) for choice in chunk.get('choices', []): print(choice.get('delta', {}).get('content') or '', end='', flush=True) if chunk.get('usage'): print('\nusage:', chunk['usage']) return False for raw in response: line = raw.decode('utf-8').rstrip('\r\n') if line == '': if consume(): return event.clear() elif line.startswith('data:'): event.append(line[5:].lstrip(' ')) if event and consume(): return raise RuntimeError('Stream interrupted. Keep request ID and idempotency key; do not create another submission.') def finish(result): deadline = time.monotonic() + 300 while result.get('task_id') and result.get('status') not in ('succeeded', 'failed', 'unknown'): if time.monotonic() >= deadline: print(result) raise TimeoutError('Keep task_id and query later; do not resubmit.') time.sleep(2) with request('tasks/' + result['task_id']) as response: result = json.load(response) print(json.dumps(result, indent=2)) if result.get('status') == 'unknown': raise RuntimeError('Outcome unconfirmed. Retain IDs and wait for reconciliation; do not resubmit.') if result.get('status') == 'failed': raise RuntimeError('Generation failed. Inspect error and billing before a new logical request.') if __name__ == '__main__': path, payload = CASES[MODE] idem = os.environ.get('XXXROUTER_REQUEST_ID') or str(uuid.uuid4()) print('idempotency_key=', idem, file=sys.stderr) # Persist the key with this exact payload before submitting in a real application. with request(path, payload, idem) as response: print('request_id=', response.headers.get('X-Request-ID'), file=sys.stderr) if 'text/event-stream' in response.headers.get('Content-Type', ''): stream(response) else: finish(json.load(response)) ``` ## 相关文档 [chat](/docs?page=chat&lang=zh-CN) · [images](/docs?page=images&lang=zh-CN) · [videos](/docs?page=videos&lang=zh-CN) · [tasks](/docs?page=tasks&lang=zh-CN) · [uploads](/docs?page=uploads&lang=zh-CN) · [errors](/docs?page=errors&lang=zh-CN) [语言官方文档](https://docs.python.org/3/library/urllib.request.html) · 2026-10-26 --- # TypeScript / Node.js 接入 请在服务端运行示例,使用语言自带 HTTP 客户端,不依赖平台 SDK。只有执行运行命令才会发送请求;模型须先完成配置并启用。 ## 环境准备 运行环境: **Node.js 22.18+**. [下载完整源码](/examples/typescript.ts). 设置部署地址,末尾仅包含一次 `/v1`,并通过环境变量提供服务端 API Key。`https://xxxrouter.com` 是正式服务域名;本地测试请替换为你的部署 origin。当前尚未部署,联调认证尚未完成。 ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 选择一种调用方式 `chat` 读取 SSE 增量内容和最终用量;`image` 接收同步图片;`async-image` 和 `video` 处理 HTTP 202 并查询返回的任务。轮询约五分钟后停止(另加正在进行的读请求时间);超时后保留任务 ID,稍后继续查询。 ```sh node --experimental-strip-types typescript.ts chat # node --experimental-strip-types typescript.ts image # node --experimental-strip-types typescript.ts async-image # node --experimental-strip-types typescript.ts video ``` ## 幂等与异常恢复 每次重新运行默认创建新的幂等键,可能产生一笔新费用。应用应在提交前持久化幂等键和完整请求体。仅在恢复同一个逻辑请求、且模型、请求体、API Key 和来源完全一致时,设置 `XXXROUTER_REQUEST_ID` 为原值;网络错误后不要换新键重跑。这些客户端只发起一次生成 POST,不会自动重试生成。 保存响应头 `X-Request-ID`。HTTP 错误会保留响应正文供排查。任务 `status=unknown` 时停止自动轮询,稍后读取或等待对账,不视为成功或免费调用。流中的错误事件或缺少 `[DONE]` 均表示中断;没有用量不等于零费用。断开连接不取消已接受的任务。不要记录 API Key 或签名媒体 URL。 ## 参考图片与结果文件 模型支持参考图时,通过 `image` 传入公开可访问的 HTTPS 地址。公共推理 API 接收 URL,不接收文件字节。Playground 可将临时输入图片或视频上传到私有 R2,仅登录且已验证的账户可用。XXXRouter 不下载、保存或转存生成结果。 成功响应原样返回生成结果的原始 URL。链接可用性、有效期和浏览器跨域行为由原始主机决定。请在到期前保存结果;查询已完成任务只返回已记录的 URL,不会续期。本站可为请求恢复保留元数据和结果 URL,但不保留生成结果文件。 ## 完整示例 这些代码是接入示例,不代表真实模型或支付渠道已启用。请勿打包进浏览器端。 ```typescript // Node.js 22.18+; run: node --experimental-strip-types typescript.ts chat // Uses native fetch, including explicit HTTP and SSE error handling. import { randomUUID } from 'node:crypto'; type Result = { status?: string; task_id?: string; error?: unknown; [key: string]: any }; const base = process.env.XXXROUTER_BASE_URL?.replace(/\/$/, ''); // Include /v1. const key = process.env.XXXROUTER_API_KEY; if (!base || !key) throw new Error('Set XXXROUTER_BASE_URL and XXXROUTER_API_KEY'); const cases: Record]> = { chat: ['chat/completions', { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'Explain HTTP in one sentence.' }], max_tokens: 256, stream: true, stream_options: { include_usage: true } }], image: ['images/generations', { model: 'seedream-4.5', prompt: 'A ceramic cup in natural light', size: '2048x2048', n: 1 }], 'async-image': ['images/generations', { model: 'nano-banana-pro', prompt: 'A ceramic cup in natural light', size: '1024x1024', n: 1 }], video: ['videos/generations', { model: 'kling-3.0', prompt: 'A slow camera move across a mountain landscape', duration: 5, aspect_ratio: '16:9' }], }; async function request(path: string, payload?: unknown, idem?: string): Promise { const headers: Record = { Authorization: `Bearer ${key}` }; if (idem) headers['Idempotency-Key'] = idem; if (payload) headers['Content-Type'] = 'application/json'; const response = await fetch(`${base}/${path}`, { method: payload ? 'POST' : 'GET', headers, redirect: 'error', body: payload ? JSON.stringify(payload) : undefined, signal: AbortSignal.timeout(payload ? 300_000 : 30_000), }); console.error('request_id=', response.headers.get('X-Request-ID')); if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); return response; } async function stream(response: Response): Promise { if (!response.body) throw new Error('Empty response body'); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ''; let data: string[] = []; const consume = (): boolean => { const value = data.join('\n'); data = []; if (value === '[DONE]') return true; if (!value) return false; const chunk = JSON.parse(value); if (chunk.error) throw new Error(JSON.stringify(chunk.error)); for (const choice of chunk.choices ?? []) process.stdout.write(choice.delta?.content ?? ''); if (chunk.usage) console.log('\nusage:', chunk.usage); return false; }; try { while (true) { const { value, done } = await reader.read(); buffer += value ?? ''; let end: number; while ((end = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, end).replace(/\r$/, ''); buffer = buffer.slice(end + 1); if (!line && consume()) return; if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, '')); } if (done) break; } if (buffer.startsWith('data:')) data.push(buffer.slice(5).trimStart()); if (consume()) return; throw new Error('Stream interrupted. Keep request ID and idempotency key; do not resubmit.'); } finally { await reader.cancel(); reader.releaseLock(); } } async function finish(result: Result): Promise { const deadline = Date.now() + 300_000; while (result.task_id && !['succeeded', 'failed', 'unknown'].includes(result.status ?? '')) { if (Date.now() >= deadline) throw new Error(`Query task ${result.task_id} later; do not resubmit`); await new Promise(resolve => setTimeout(resolve, 2000)); result = await (await request(`tasks/${encodeURIComponent(result.task_id)}`)).json(); } console.log(result); if (result.status === 'unknown') throw new Error('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (result.status === 'failed') throw new Error('Generation failed; inspect error and billing.'); } const [path, payload] = cases[process.argv[2] ?? 'chat']; const idem = process.env.XXXROUTER_REQUEST_ID || randomUUID(); console.error('idempotency_key=', idem); // Persist alongside the exact payload before submitting. const response = await request(path, payload, idem); if (response.headers.get('content-type')?.includes('text/event-stream')) await stream(response); else await finish(await response.json()); ``` ## 相关文档 [chat](/docs?page=chat&lang=zh-CN) · [images](/docs?page=images&lang=zh-CN) · [videos](/docs?page=videos&lang=zh-CN) · [tasks](/docs?page=tasks&lang=zh-CN) · [uploads](/docs?page=uploads&lang=zh-CN) · [errors](/docs?page=errors&lang=zh-CN) [语言官方文档](https://nodejs.org/api/globals.html#fetch) · 2026-10-26 --- # Go 接入 请在服务端运行示例,使用语言自带 HTTP 客户端,不依赖平台 SDK。只有执行运行命令才会发送请求;模型须先完成配置并启用。 ## 环境准备 运行环境: **Go 1.22+**. [下载完整源码](/examples/go.go). 设置部署地址,末尾仅包含一次 `/v1`,并通过环境变量提供服务端 API Key。`https://xxxrouter.com` 是正式服务域名;本地测试请替换为你的部署 origin。当前尚未部署,联调认证尚未完成。 ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 选择一种调用方式 `chat` 读取 SSE 增量内容和最终用量;`image` 接收同步图片;`async-image` 和 `video` 处理 HTTP 202 并查询返回的任务。轮询约五分钟后停止(另加正在进行的读请求时间);超时后保留任务 ID,稍后继续查询。 ```sh go run go.go chat # go run go.go image # go run go.go async-image # go run go.go video ``` ## 幂等与异常恢复 每次重新运行默认创建新的幂等键,可能产生一笔新费用。应用应在提交前持久化幂等键和完整请求体。仅在恢复同一个逻辑请求、且模型、请求体、API Key 和来源完全一致时,设置 `XXXROUTER_REQUEST_ID` 为原值;网络错误后不要换新键重跑。这些客户端只发起一次生成 POST,不会自动重试生成。 保存响应头 `X-Request-ID`。HTTP 错误会保留响应正文供排查。任务 `status=unknown` 时停止自动轮询,稍后读取或等待对账,不视为成功或免费调用。流中的错误事件或缺少 `[DONE]` 均表示中断;没有用量不等于零费用。断开连接不取消已接受的任务。不要记录 API Key 或签名媒体 URL。 ## 参考图片与结果文件 模型支持参考图时,通过 `image` 传入公开可访问的 HTTPS 地址。公共推理 API 接收 URL,不接收文件字节。Playground 可将临时输入图片或视频上传到私有 R2,仅登录且已验证的账户可用。XXXRouter 不下载、保存或转存生成结果。 成功响应原样返回生成结果的原始 URL。链接可用性、有效期和浏览器跨域行为由原始主机决定。请在到期前保存结果;查询已完成任务只返回已记录的 URL,不会续期。本站可为请求恢复保留元数据和结果 URL,但不保留生成结果文件。 ## 完整示例 这些代码是接入示例,不代表真实模型或支付渠道已启用。请勿打包进浏览器端。 ```go // Go 1.22+, standard library only. Run: go run go.go chat package main import ( "bufio" "bytes" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) type Object = map[string]any var base = strings.TrimRight(os.Getenv("XXXROUTER_BASE_URL"), "/") // Include /v1. var key = os.Getenv("XXXROUTER_API_KEY") var client = &http.Client{Timeout: 300 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} func request(path string, payload Object, idem string) (*http.Response, error) { var body io.Reader method := "GET" if payload != nil { raw, err := json.Marshal(payload) if err != nil { return nil, err } body = bytes.NewReader(raw) method = "POST" } req, err := http.NewRequest(method, base+"/"+path, body) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) if payload != nil { req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", idem) } response, err := client.Do(req) if err != nil { return nil, err } fmt.Fprintln(os.Stderr, "request_id=", response.Header.Get("X-Request-ID")) if response.StatusCode < 200 || response.StatusCode >= 300 { defer response.Body.Close() raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20)) return nil, fmt.Errorf("HTTP %d: %s", response.StatusCode, raw) } return response, nil } func decode(response *http.Response) (Object, error) { defer response.Body.Close() var result Object err := json.NewDecoder(response.Body).Decode(&result) return result, err } func stream(response *http.Response) error { defer response.Body.Close() scanner := bufio.NewScanner(response.Body) scanner.Buffer(make([]byte, 4096), 2<<20) var parts []string consume := func() (bool, error) { data := strings.Join(parts, "\n") parts = nil if data == "[DONE]" { return true, nil } if data == "" { return false, nil } var chunk struct { Error json.RawMessage `json:"error"` Usage json.RawMessage `json:"usage"` Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` } if err := json.Unmarshal([]byte(data), &chunk); err != nil { return false, err } if len(chunk.Error) > 0 && string(chunk.Error) != "null" { return false, fmt.Errorf("stream error: %s", chunk.Error) } for _, choice := range chunk.Choices { fmt.Print(choice.Delta.Content) } if len(chunk.Usage) > 0 && string(chunk.Usage) != "null" { fmt.Printf("\nusage: %s\n", chunk.Usage) } return false, nil } for scanner.Scan() { line := scanner.Text() if line == "" { done, err := consume() if err != nil || done { return err } } if strings.HasPrefix(line, "data:") { parts = append(parts, strings.TrimPrefix(line[5:], " ")) } } if err := scanner.Err(); err != nil { return err } done, err := consume() if err != nil || done { return err } return fmt.Errorf("stream interrupted; keep request ID and idempotency key, do not resubmit") } func finish(result Object) error { deadline := time.Now().Add(5 * time.Minute) for { task, hasTask := result["task_id"].(string) status, _ := result["status"].(string) if !hasTask || status == "succeeded" || status == "failed" || status == "unknown" { break } if time.Now().After(deadline) { return fmt.Errorf("query task %s later; do not resubmit", task) } time.Sleep(2 * time.Second) response, err := request("tasks/"+task, nil, "") if err != nil { return err } result, err = decode(response) if err != nil { return err } } raw, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(raw)) if result["status"] == "unknown" { return fmt.Errorf("outcome unconfirmed; retain IDs and wait for reconciliation") } if result["status"] == "failed" { return fmt.Errorf("generation failed; inspect error and billing") } return nil } func run() error { if base == "" || key == "" { return fmt.Errorf("set XXXROUTER_BASE_URL and XXXROUTER_API_KEY") } mode := "chat" if len(os.Args) > 1 { mode = os.Args[1] } path := "images/generations" var payload Object switch mode { case "chat": path = "chat/completions" payload = Object{"model": "deepseek-v4-flash", "messages": []Object{{"role": "user", "content": "Explain HTTP in one sentence."}}, "max_tokens": 256, "stream": true, "stream_options": Object{"include_usage": true}} case "image": payload = Object{"model": "seedream-4.5", "prompt": "A ceramic cup in natural light", "size": "2048x2048", "n": 1} case "async-image": payload = Object{"model": "nano-banana-pro", "prompt": "A ceramic cup in natural light", "size": "1024x1024", "n": 1} case "video": path = "videos/generations" payload = Object{"model": "kling-3.0", "prompt": "A slow camera move across a mountain landscape", "duration": 5, "aspect_ratio": "16:9"} default: return fmt.Errorf("choose chat, image, async-image or video") } idem := os.Getenv("XXXROUTER_REQUEST_ID") if idem == "" { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return err } idem = hex.EncodeToString(b) } fmt.Fprintln(os.Stderr, "idempotency_key=", idem) // Persist with the exact payload before submission. response, err := request(path, payload, idem) if err != nil { return err } if strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { return stream(response) } result, err := decode(response) if err != nil { return err } return finish(result) } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } ``` ## 相关文档 [chat](/docs?page=chat&lang=zh-CN) · [images](/docs?page=images&lang=zh-CN) · [videos](/docs?page=videos&lang=zh-CN) · [tasks](/docs?page=tasks&lang=zh-CN) · [uploads](/docs?page=uploads&lang=zh-CN) · [errors](/docs?page=errors&lang=zh-CN) [语言官方文档](https://pkg.go.dev/net/http) · 2026-10-26 --- # PHP 接入 请在服务端运行示例,使用语言自带 HTTP 客户端,不依赖平台 SDK。只有执行运行命令才会发送请求;模型须先完成配置并启用。 ## 环境准备 运行环境: **PHP 8.1+ + ext-curl**. [下载完整源码](/examples/php.php). 设置部署地址,末尾仅包含一次 `/v1`,并通过环境变量提供服务端 API Key。`https://xxxrouter.com` 是正式服务域名;本地测试请替换为你的部署 origin。当前尚未部署,联调认证尚未完成。 ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 选择一种调用方式 `chat` 读取 SSE 增量内容和最终用量;`image` 接收同步图片;`async-image` 和 `video` 处理 HTTP 202 并查询返回的任务。轮询约五分钟后停止(另加正在进行的读请求时间);超时后保留任务 ID,稍后继续查询。 ```sh php php.php chat # php php.php image # php php.php async-image # php php.php video ``` ## 幂等与异常恢复 每次重新运行默认创建新的幂等键,可能产生一笔新费用。应用应在提交前持久化幂等键和完整请求体。仅在恢复同一个逻辑请求、且模型、请求体、API Key 和来源完全一致时,设置 `XXXROUTER_REQUEST_ID` 为原值;网络错误后不要换新键重跑。这些客户端只发起一次生成 POST,不会自动重试生成。 保存响应头 `X-Request-ID`。HTTP 错误会保留响应正文供排查。任务 `status=unknown` 时停止自动轮询,稍后读取或等待对账,不视为成功或免费调用。流中的错误事件或缺少 `[DONE]` 均表示中断;没有用量不等于零费用。断开连接不取消已接受的任务。不要记录 API Key 或签名媒体 URL。 ## 参考图片与结果文件 模型支持参考图时,通过 `image` 传入公开可访问的 HTTPS 地址。公共推理 API 接收 URL,不接收文件字节。Playground 可将临时输入图片或视频上传到私有 R2,仅登录且已验证的账户可用。XXXRouter 不下载、保存或转存生成结果。 成功响应原样返回生成结果的原始 URL。链接可用性、有效期和浏览器跨域行为由原始主机决定。请在到期前保存结果;查询已完成任务只返回已记录的 URL,不会续期。本站可为请求恢复保留元数据和结果 URL,但不保留生成结果文件。 ## 完整示例 这些代码是接入示例,不代表真实模型或支付渠道已启用。请勿打包进浏览器端。 ```php getMessage(); } }; $curl = curl_init($base . '/' . $path); $options = [ CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $payload !== null ? 300 : 30, CURLOPT_HEADERFUNCTION => function ($handle, string $line) use (&$contentType, &$requestId): int { if (stripos($line, 'Content-Type:') === 0) $contentType = trim(substr($line, 13)); if (stripos($line, 'X-Request-ID:') === 0) $requestId = trim(substr($line, 13)); return strlen($line); }, CURLOPT_WRITEFUNCTION => function ($handle, string $bytes) use (&$contentType, &$raw, &$buffer, &$data, &$done, &$streamError, $consume): int { if (!str_contains($contentType, 'text/event-stream')) { $raw .= $bytes; return strlen($bytes); } $buffer .= $bytes; while (($end = strpos($buffer, "\n")) !== false) { $line = rtrim(substr($buffer, 0, $end), "\r"); $buffer = substr($buffer, $end + 1); if ($line === '') $consume(); elseif (str_starts_with($line, 'data:')) $data[] = ltrim(substr($line, 5), ' '); } return strlen($bytes); }, ]; if ($payload !== null) { $headers[] = 'Content-Type: application/json'; $headers[] = 'Idempotency-Key: ' . $idem; $options[CURLOPT_POST] = true; $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_THROW_ON_ERROR); } $options[CURLOPT_HTTPHEADER] = $headers; curl_setopt_array($curl, $options); try { $ok = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); fwrite(STDERR, 'request_id=' . $requestId . "\n"); if ($ok === false) throw new RuntimeException('Transport error; retain IDs, do not resubmit: ' . curl_error($curl)); if ($status < 200 || $status >= 300) throw new RuntimeException('HTTP ' . $status . ': ' . $raw); if (str_contains($contentType, 'text/event-stream')) { if (str_starts_with($buffer, 'data:')) $data[] = ltrim(substr($buffer, 5), ' '); $consume(); if ($streamError !== null) throw new RuntimeException($streamError); if (!$done) throw new RuntimeException('Stream interrupted; retain request ID and idempotency key.'); return ['stream_complete' => true]; } return json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } finally { curl_close($curl); } } $cases = [ 'chat' => ['chat/completions', ['model'=>'deepseek-v4-flash','messages'=>[['role'=>'user','content'=>'Explain HTTP in one sentence.']],'max_tokens'=>256,'stream'=>true,'stream_options'=>['include_usage'=>true]]], 'image' => ['images/generations', ['model'=>'seedream-4.5','prompt'=>'A ceramic cup in natural light','size'=>'2048x2048','n'=>1]], 'async-image' => ['images/generations', ['model'=>'nano-banana-pro','prompt'=>'A ceramic cup in natural light','size'=>'1024x1024','n'=>1]], 'video' => ['videos/generations', ['model'=>'kling-3.0','prompt'=>'A slow camera move across a mountain landscape','duration'=>5,'aspect_ratio'=>'16:9']], ]; $mode = $argv[1] ?? 'chat'; if (!isset($cases[$mode])) throw new InvalidArgumentException('Choose chat, image, async-image or video'); [$path, $payload] = $cases[$mode]; $idem = getenv('XXXROUTER_REQUEST_ID') ?: bin2hex(random_bytes(16)); fwrite(STDERR, 'idempotency_key=' . $idem . "\n"); // Persist alongside exact payload before submission. $result = request($path, $payload, $idem); $deadline = microtime(true) + 300; while (isset($result['task_id']) && !in_array($result['status'], ['succeeded', 'failed', 'unknown'], true)) { if (microtime(true) >= $deadline) throw new RuntimeException('Query task ' . $result['task_id'] . ' later; do not resubmit.'); sleep(2); $result = request('tasks/' . rawurlencode($result['task_id'])); } echo json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . "\n"; if (($result['status'] ?? '') === 'unknown') throw new RuntimeException('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (($result['status'] ?? '') === 'failed') throw new RuntimeException('Generation failed; inspect error and billing.'); ``` ## 相关文档 [chat](/docs?page=chat&lang=zh-CN) · [images](/docs?page=images&lang=zh-CN) · [videos](/docs?page=videos&lang=zh-CN) · [tasks](/docs?page=tasks&lang=zh-CN) · [uploads](/docs?page=uploads&lang=zh-CN) · [errors](/docs?page=errors&lang=zh-CN) [语言官方文档](https://www.php.net/manual/en/function.curl-exec.php) · 2026-10-26 --- # 更新记录 ## 0.1.0 — 2026-10-26 — 实施草案 - 增加账户美元余额与分 Key 消费控制。 - 增加 en / zh-CN 开发者文档和机器可读 OpenAPI 草案。 - 区分同步直接图片结果与异步图片/视频任务。 - 定义 OpenAI 风格文本对话及可选 SSE。 - 增加 $0.20、7 天有效试用赠金及 Payblis / NOWPayments 托管支付接入。 - 默认请求日志仅保留元数据,期限 30 天。 本文描述已实现接口;部署、真实模型路由认证及支付渠道验收仍是独立的发布要求。 [快速开始](/docs?page=quickstart&lang=zh-CN) --- # クイックスタート > 実装リファレンス · 2026-10-26 · リクエストを送信する前に、デプロイ環境を設定してください。 本アプリケーションでは、認証、残高、リクエストをサーバー側で管理します。`https://xxxrouter.com` は正式なサービスドメインです。ローカルテストでは、ご自身のデプロイ先のオリジンに置き換えてください。現時点ではサービスは未デプロイで、連携の認証も完了していません。ゲートウェイを通じて、同一オリジンの `/v1` にアクセスできます。モデルは初期状態で無効です。運営者が認証情報を設定し、ルートの動作を確認して価格を設定した後に有効化します。本リファレンスは、デプロイや結合テストの完了を示すものではありません。 1. コンソールにログインし、USD 残高を確認します。 2. API キーを作成します。作成時に一度だけコピーし、サーバーのシークレット管理システムに保存します。 3. モデルのレスポンスモードと適用価格を確認します。 4. 自分のバックエンドから API を呼び出します。返されたリクエスト ID を使い、Usage & Logs で結果を追跡します。 ```sh export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' curl https://xxxrouter.com/v1/models \ -H "Authorization: Bearer $XXXROUTER_API_KEY" ``` ```sh curl https://xxxrouter.com/v1/chat/completions \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-chat-request-001' \ -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Hello"}],"max_tokens":256,"stream":false}' ``` チャットは OpenAI 形式の契約に従います。画像 API は結果の直接返却とタスク形式のレスポンスの両方に対応しますが、すべてのモデルが OpenAI Images の完全な置き換えになるわけではありません。コンソールの Playground は、ログイン中のセッションを通じて外部 API 呼び出しと同じ価格・会計ルールを使用し、API キーとは別に利用元を記録します。 次に読む:[認証](/docs?page=auth&lang=ja)、[モデルの機能](/docs?page=models&lang=ja)、[画像生成](/docs?page=images&lang=ja)、[エラー](/docs?page=errors&lang=ja)。機械可読形式:[OpenAPI](/openapi.json)、[llms.txt](/llms.txt)、[全文](/llms-full.txt)。 --- # 認証 生成、タスク、キーのリクエストには `Authorization: Bearer ` を送信します。モデルカタログの GET エンドポイントでは、認証なしで公開価格を確認できます。有効なキーを付けると、アカウントに適用される価格を取得できます。キーをクエリ文字列、公開リポジトリ、クライアントのビルド成果物、共有するスクリーンショット、ブラウザストレージに保存しないでください。ログインとメール認証を済ませたコンソールのアカウントで、実際のキーを作成します。 キーの全文は作成時に一度だけ表示されます。その後は先頭・末尾の一部と変更されない ID のみが表示されます。サーバーはキー付きの一方向ダイジェストを保存し、復元可能な平文は保存しません。キーの管理にはコンソールセッションが必要です。推論用キーで別のキーを作成したり、アカウントの請求設定を変更したりすることはできません。 キーをローテーションするには、代替キーの作成、デプロイ、通信の確認、元のキーの失効の順に行います。一時停止は解除できますが、失効は取り消せません。有効期限は新しいリクエストごとに UTC で判定します。一時停止、期限切れ、失効の前に受け付けたタスクの課金と履歴は維持されます。確認には同じアカウントの別の有効なキー、またはコンソールを使用してください。 `GET /v1/key` は、現在のキーのメタデータを返します。適用中の制限と残りの利用可能額を確認でき、シークレット自体は返しません。エラーから、推測したキーが別のアカウントに属しているかどうかを判別することはできません。 [API キーの管理](/docs?page=keys&lang=ja) · [エラー](/docs?page=errors&lang=ja) --- # モデルと価格 `GET /v1/models` は、アカウントで利用できるモデルの一覧を返します。`GET /v1/models/{id}` は、機能、パラメータ、レスポンスモード、利用可否、適用価格を返します。公開モデル ID は、ルーティング用のプレフィックスを含まないプラットフォーム固有の名前です。例:`/v1/models/seedream-4.5`。ルーティングやインフラのメタデータは内部情報であり、公開モデル API には含まれません。 | サンプルモデル ID | 出力 | レスポンスモード | |---|---|---| | `seedream-4.5` | 画像 | `sync` | | `nano-banana-pro` | 画像 | `async` | | `flux-2-pro` | 画像 | `async` | | `kling-3.0` | 動画 | `async` | | `deepseek-v4-flash` | テキスト | カタログ上のモードは `stream`。`stream` により JSON または SSE を選択 | これらはプラットフォームの公開モデル ID であり、実際の利用に必要な設定や動作検証が完了していることを保証するものではありません。モデルのレスポンスモードはカタログのメタデータで明示されます。URL の一部やプログラミング言語の `async` キーワードから推測してはいけません。モデルの機能とパラメータは、資金の仮押さえや上流への送信前に検証されます。 価格は USD 建てです。画像の料金は対応するパラメータに応じて決まり、このリリースでは動画は長さに応じて課金します。LLM は入力トークンと出力トークンを別々に課金します。リクエストの受付時に適用価格のバージョンを保存するため、その後カタログの価格を変更しても既存リクエストには影響しません。見積額と確定額は異なります。 [画像生成のプロトコル](/docs?page=images&lang=ja) · [残高と課金](/docs?page=billing&lang=ja) ## 適用価格の優先順位 条件に一致するルールを、ユーザー × モデルの個別価格、価格グループ × モデルの個別価格、価格グループの標準割引、公開価格の順で、必ず一つだけ適用します。個別価格には固定価格、またはそのモデルで許可された範囲内の割引を設定できます。ルールを重ねて適用することはありません。原価を下回る価格には運営者の明示的な確認が必要です。供給元の原価が不明な場合、推測した利益率を表示してはいけません。選択したルール、単位、バージョンはリクエスト受付時に固定します。 モデルの `parameters` リストは、プラットフォームで正規化したトップレベルフィールドを示します。画像は `size`、`n`、`image`、動画は `duration`、`aspect_ratio`、`image`、チャットは `temperature`、`max_tokens`、`stream`、`stream_options` を使用します。任意のパラメータをそのまま転送するための辞書ではありません。Seedream 4.5 の例では `2048x2048` を使用します。本番環境のパラメータ制限はモデルごとに確認する必要があります。UI のサンプル選択肢は、実際に利用できる機能を保証するものではありません。 モデルの JSON には name、modality(llm/image/video)、mode/response_mode(stream/sync/async)、parameters、sizes、supportsImage、enabled を使用します。pricing は currency、price、null を許容する outputPrice を含むオブジェクトで、金額は小数点以下 6 桁の USD 文字列です。トップレベルの price/outputPrice は数値形式の USD 金額です。無効なモデルは呼び出せません。初期データの価格は運営者による確認が必要であり、不明な原価から利益率を表示することはありません。 --- # チャット(Chat completions) `POST /v1/chat/completions` は、`model`、`messages`、`max_tokens`、`temperature`、`stream`、任意の `stream_options: {"include_usage": true}` を受け付けます。v0.1 の契約は、`system`、`user`、`assistant` ロールのテキストメッセージを対象とします。ツール呼び出し、マルチモーダルチャット、embeddings、Responses API は、このリリースの契約に含まれません。 ```json {"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Explain HTTP in one sentence."}],"max_tokens":256,"temperature":0.7,"stream":true,"stream_options":{"include_usage":true}} ``` 非ストリーミングの結果には、`id`、`object: "chat.completion"`、`model`、`choices`、`usage` が含まれます。ストリーミングは `text/event-stream` を返し、各 data イベントの `choices[].delta` に補完内容の差分が含まれます。`stream_options.include_usage=true` の場合、上流から最終利用量イベントが提供されると、`data: [DONE]` より前に返されます。レスポンスヘッダーの `X-Request-ID` を記録してください。SSE 全体を一つの JSON ドキュメントとして解析してはいけません。 ゲートウェイは JSON 出力を強制するシステムプロンプトを挿入したり、通知せずにモデルを切り替えたり、出力修正のために課金済みの補完を再実行したりしてはいけません。接続の切断や Stop の操作によって、すでに生成されたトークンの利用量がなくなることはありません。最終課金は確認できた利用量に基づきます。確定に必要な利用量情報がない場合は照合待ちとし、暗黙にゼロとして扱いません。各ルートは有効化前に結合動作の検証が必要です。 [残高と課金](/docs?page=billing&lang=ja) · [本番環境への接続](/docs?page=production&lang=ja) すべての生成 POST リクエストには Idempotency-Key(1~128 文字の印字可能な ASCII 文字)が必要です。新しい論理リクエストには新しい値を使い、まったく同じリクエストの再照会に限って同じ値を再利用します。max_tokens は 1~4096、temperature は 0~2 です。ストリーミングの再照会で、課金済みの上流ストリームを再開または再生することはありません。 --- # 画像生成 `POST /v1/images/generations` には、モデルカタログによって選択される二つのレスポンスモードがあります。必ずモデルのメタデータを確認し、文書化されたレスポンスに応じて処理してください。どちらのモードも `model`、`prompt`、任意のトップレベル参考画像 URL `image`、`size`、`n` を受け付けます。これらはプラットフォームで正規化したフィールドであり、アダプターが動作検証済みモデルのスキーマに変換します。 ```sh curl https://xxxrouter.com/v1/images/generations \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-image-request-001' \ -d '{"model":"seedream-4.5","prompt":"A ceramic cup in natural light","size":"2048x2048","n":1}' ``` ## 同期モデル:HTTP 200 ```json {"request_id":"req_demo_sync","model":"seedream-4.5","response_mode":"sync","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/cup.png"}],"billing":{"status":"settled","currency":"USD","amount":"0.030000"}} ``` 画像は元の HTTP レスポンスで返されます。上流のタスク ID や、後続のタスクポーリングは不要です。上記の金額はサンプルであり、見積価格ではありません。同期処理に時間がかかっても同期モードのままです。クライアントのタイムアウトは失敗の証明ではなく、再送信してよいことを意味しません。 ## 非同期モデル:HTTP 202 `nano-banana-pro` または `flux-2-pro` を選択すると、次の形式で返されます。 ```json {"request_id":"req_demo_async","task_id":"task_demo_01","model":"nano-banana-pro","response_mode":"async","status":"running"} ``` [タスクエンドポイント](/docs?page=tasks&lang=ja)をポーリングし、終了状態になるまで確認します。`202` は生成完了を意味しません。画像用の共通ルートを使用していても、すべてのモデルが OpenAI Images と互換であるとは限りません。設定済みのルートは実際のリクエストを送信します。認証情報が不足している場合、課金処理を模した成功結果は返しません。 [参考画像の入力](/docs?page=uploads&lang=ja) · [エラー](/docs?page=errors&lang=ja) 現在のサービスでは n=1 のみをサポートします。image はモデルの parameters に含まれている場合に限り使用できます。 成功応答は生成結果の元の URL を変更せず返します。有効期限、可用性、クロスオリジン制限は元のホストが管理します。期限前に結果を保存してください。完了タスクの再取得では URL は延長されません。復旧用のメタデータと URL は保持する場合がありますが、生成ファイルは保存しません。 --- # 動画生成 `POST /v1/videos/generations` は HTTP 202 とタスクを返します。公開モデル ID は `kling-3.0` です。実際に利用するには、ルートの設定と動作検証が必要です。 ```json {"model":"kling-3.0","prompt":"A slow camera movement around a ceramic cup","duration":5,"aspect_ratio":"16:9"} ``` 論理的な送信ごとに `Idempotency-Key` を付けます。`task_id` を保存し、`/v1/tasks/{task_id}` をポーリングしてください。成功結果には動画 URL、メディア種別、最終課金情報が含まれます。上流が対応していない限り、キャンセルや進捗率を保証してはいけません。信頼できる根拠がない場合、正確な進捗率は表示しません。 見積もりに使用する長さは料金計算の入力値であり、生成される動画が厳密にその長さになることを保証するものではありません。公開前に、各ルートの出力時間とパラメータの組み合わせに対する課金方法を文書化する必要があります。別モデルへの自動フォールバックは行う前提になっていません。 [タスク](/docs?page=tasks&lang=ja) · [残高と課金](/docs?page=billing&lang=ja) 正規化されたトップレベルパラメータは `duration`、`aspect_ratio`、任意の `image` です。現在のスキーマは 5/10 秒と 16:9、9:16、1:1 を受け付けます。呼び出す前にモデルの利用可否を確認してください。このリクエスト草案に `resolution` や入れ子の `parameters` フィールドはありません。 成功応答は生成結果の元の URL を変更せず返します。有効期限、可用性、クロスオリジン制限は元のホストが管理します。期限前に結果を保存してください。完了タスクの再取得では URL は延長されません。復旧用のメタデータと URL は保持する場合がありますが、生成ファイルは保存しません。 --- # タスクと冪等性 `GET /v1/tasks/{id}` は、受け付け済みの画像・動画タスクを取得します。取得のたびに認証とアカウントの所有権を確認します。同じアカウントの別の有効なキーでもタスクを確認できますが、利用実績の帰属先となる元のキー ID は変更されません。 状態遷移:`submitting → running → succeeded | failed`。結果が不明な場合、リクエストは `status: "unknown"`、課金は `billing.status: "reconciling"` となり、自動再送信は禁止です。タスクの結果と課金の状態は別々に扱います。結果を取得できても、費用は照合中である場合があります。 ```json {"task_id":"task_demo_01","request_id":"req_demo_async","model":"nano-banana-pro","response_mode":"async","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/image.png","media_type":"image"}],"billing":{"status":"settled","currency":"USD","amount":"0.150000"}} ``` ポーリング間隔には上限を設けてください。最初は 2 秒間隔とし、最大 10 秒まで間隔を延ばすことを推奨します。`Retry-After` に従ってください。GET を再試行しても、新しい生成は開始されません。POST のレスポンスを受け取れなかった場合は、同じ冪等キーとまったく同じペイロードを再利用します。同じアカウント、エンドポイント、キー、リクエスト指紋の組み合わせには元の記録を返し、ペイロードが異なる場合は `409 IDEMPOTENCY_CONFLICT` を返します。冪等性の記録は MySQL に永続保存されます。日付が変わったことを理由に、古いキーの再利用や結果不明なリクエストの再送信が安全になると考えてはいけません。 同期画像の再照会では、保存済みの直接結果があればそれを返します。処理中または結果不明の同期リクエストには、HTTP `409` と現在のリクエスト記録を返し、上流への再送信は行いません。同期リクエスト ID をタスク ID として `/tasks` に渡してポーリングしてはいけません。 [エラー](/docs?page=errors&lang=ja) · [本番環境への接続](/docs?page=production&lang=ja) 成功応答は生成結果の元の URL を変更せず返します。有効期限、可用性、クロスオリジン制限は元のホストが管理します。期限前に結果を保存してください。完了タスクの再取得では URL は延長されません。復旧用のメタデータと URL は保持する場合がありますが、生成ファイルは保存しません。 --- # 残高と課金 アカウントには一つの前払い USD 残高があります。キーの利用上限は支出を制限するものであり、独立したウォレットではありません。利用可能額、仮押さえ額、確定額は明確に区別して表示します。金額は整数のマイクロドルまたは正確な十進数で保存し、二進浮動小数点数は使用しません。 試用特典は、有効期間 7 日間の $0.20 のプロモーションクレジットです。有料で入金した資金とは別に管理され、現金ではありません。サーバー側で受給資格と一度限りの付与を管理します。メールアドレスを確認し、認証チャレンジを完了してください。特典の有効期限は、有料資金とは独立して適用されます。 リクエストを受け付ける前に、十分なアカウント資金とキーの利用枠を一つの不可分な処理として仮押さえします。完了時に実際の金額を確定し、差額を解放します。課金対象の利用量がない失敗では、仮押さえを解放します。上流での処理結果が不明な場合は照合を続け、二度目の課金につながる再実行は行いません。タイムアウトやストリームの停止だけでは、利用量がゼロであると判断できません。 対応する決済方法は Payblis と NOWPayments のみです。設定済みの決済チャネルでは、決済事業者のホスト型チェックアウト注文を作成します。認証情報が不足している場合は利用不可を返します。成功ページへのリダイレクトだけで残高を加算することはありません。検証済みのコールバックと独立した照合処理で、通貨、金額、チャネル側の参照情報、注文の識別情報を確認した後、重複しない一件の台帳記録を追加します。 実装済みの注文状態は `creating`、`pending`、`confirming`、`paid`、`underpaid`、`expired`、`failed` です。一部入金の注文は、全額の入金が確認されるまで `underpaid` のままになります。このリリースには返金 API と自動返金処理は実装されていません。将来返金に対応する場合も、過去の取引を書き換えず、補償用の台帳記録を追加する必要があります。 [API キーの管理](/docs?page=keys&lang=ja) · [本番環境への接続](/docs?page=production&lang=ja) ## 試用特典の条件と資金の使用順序 $0.20 の特典を受け取る前に、アカウントのメールアドレスを確認し、認証チャレンジを完了します。受け取り処理は冪等で、頻度制限があります。特典は付与からちょうど 7 日後に失効し、有効なプロモーション資金を有料資金より先に使用します。試用リクエストにも同じ適用価格とキー・アカウントの制限が適用されます。仮押さえには資金の出所を記録します。特典の失効後に仮押さえを解放した場合、そのプロモーション部分は失効済みのプロモーション残高に戻り、利用可能な有料残高にはなりません。有効期限切れによって有料資金が失われることはありません。財務台帳の履歴は永続保存され、30 日間のリクエストログとは独立しています。 --- # API キーの管理 キーはコンソールで管理します。`GET /v1/key` は、現在のキーを確認する読み取り専用 API です。各キーには、変更されない ID、表示名、マスクされたシークレット、状態、作成日時、任意の有効期限、利用上限、利用実績の帰属情報があります。 リクエストには、アカウント残高とキーの残り利用枠の両方が必要です。未確定の仮押さえは両方に算入されるため、並行呼び出しが古い残高を参照して上限を超えることはありません。キーを無制限に設定していても、アカウントの資金は必要です。上限を現在の使用・仮押さえ額より低く設定すると新規リクエストが拒否されますが、過去の利用額は消えず、受け付け済みのタスクもキャンセルされません。 一時停止中のキーは新規呼び出しを拒否し、後から再開できます。失効したキーは恒久的に新規呼び出しを拒否します。有効期限が切れたキーは、所有者に残高があっても API を呼び出せません。既存タスクは継続し、送信時のキー ID を保持します。代替キーの利用履歴は、元のキーとは別に記録されます。 Playground はログイン中のセッションを使用し、利用元を `playground` として別に記録します。ユーザーの長期 API キーを公開したり、入力を求めたりすることはありません。アカウントのグラフは Playground と外部 API の利用を合算します。キーで絞り込んだグラフは該当キーの利用のみを表示し、アーカイブ済み・失効済みのキーも履歴の絞り込みに使用できます。 サーバーがシークレットの平文を返すのは作成時の一度だけです。その後の表示から復元することはできません。 [認証](/docs?page=auth&lang=ja) · [残高と課金](/docs?page=billing&lang=ja) ## 制限フィールド 各キーには一つの利用上限と、一つの期間(累計、日次、週次、月次)を選択します。期間は編集可能で、履歴を削除せずに現在の期間に該当する確定利用額を再計算します。日次は UTC の毎日 00:00、週次は月曜日 00:00 UTC、月次は毎月 1 日 00:00 UTC にリセットされます。期間が切り替わる前に受け付けたものを含め、すべての未確定の仮押さえを現在の上限に算入します。そのため、進行中のリクエストがリセットを利用して上限を回避することはありません。確定した利用額はリクエスト受付日時の期間に帰属し、累計には全履歴を含みます。モデルの許可リスト、RPM、IPv4/IPv6 アドレスおよび CIDR の許可リストは、それぞれ独立した受付条件です。モデルまたは IP の許可リストが空の場合は制限なしを意味し、UI でも明示します。IPv4 と IPv6 の両方に対応します。アカウントに対するプラットフォームの上限は引き続き適用されます。キーの残り利用枠はアカウント残高とは異なります。本番環境で制限を正しく適用するには、信頼するプロキシを適切に設定する必要があります。 GET /v1/key は id、name、mask、enabled、revoked、limit、period、expiresAt、models、rpm、ips、spent、reserved、nextReset を返します。キーの金額フィールドには USD の数値を使用します。limit=null はキーの支出上限なし、limit=0 は正の金額の支出を許可しないことを意味します。アカウント残高の制約は引き続き適用されます。シークレットは返されません。 --- # エラーと制限 ```json {"error":{"code":"KEY_BUDGET_EXCEEDED","message":"This key has insufficient spending allowance.","request_id":"req_demo_error"}} ``` | HTTP | コード | 対処方法 | |---|---|---| | 422 | `INVALID_INPUT` | 入力を修正してから再送信します。 | | 401 | `INVALID_API_KEY`, `API_KEY_INACTIVE` | 有効な認証情報を使用します。 | | 403 | `MODEL_NOT_ALLOWED`, `IP_NOT_ALLOWED` | コンソールでキーの制限を確認します。 | | 402 | `INSUFFICIENT_BALANCE`, `KEY_BUDGET_EXCEEDED` | 入金するか、権限の範囲内で利用上限を変更します。 | | 404 | `MODEL_NOT_FOUND`, `REQUEST_NOT_FOUND` | ID と所有権を確認します。 | | 409 | `IDEMPOTENCY_CONFLICT` | 元のペイロードを使用するか、新しい論理リクエストとして送信します。 | | 409 | 処理中のリクエスト記録、または `RESPONSE_CACHE_EXPIRED` | 待機または照合を行い、同じ生成を重複購入しないでください。 | | 422 | `UNSUPPORTED_PARAMETER` | モデルまたは入力の選択を修正します。 | | 429 | `RATE_LIMITED` | Retry-After に従い、呼び出し頻度を下げます。 | | 502/503 | `MODEL_UNAVAILABLE` | 再送信する前に、元のリクエスト状態を確認します。 | メッセージから機密情報は除去されます。供給元の残高、決済加盟店のシークレット、上流の生のスタックトレースは表示しません。調査には `X-Request-ID` を使用します。原因不明の上流エラーを、コンテンツポリシー違反として誤って分類してはいけません。SSE のヘッダー送信後にエラーが発生した場合は、ストリーム内のエラーイベントを送信して接続を閉じます。二つ目の HTTP レスポンスを返すことはありません。 [冪等性](/docs?page=tasks&lang=ja) · [本番環境への接続](/docs?page=production&lang=ja) --- # 本番環境への接続 本リファレンスはサービス実装に対応していますが、デプロイと結合動作の検証は別途必要です。実際のトラフィックを受け付ける前に、独立したテスト環境で、各上流ルート、パラメータスキーマ、価格計算、ストリーミングの利用量、決済署名、障害復旧経路を確認してください。 - サーバー側のキー、TLS、明示的なタイムアウト、上限のあるポーリング、リクエスト ID を使用します。 - 送信前に冪等性の記録を永続保存します。通信障害によって結果が不明な場合、課金対象の上流 POST を無条件に再試行してはいけません。 - ウォレットの台帳、仮押さえ、キーの予算、決済の入金確定は MySQL のトランザクションで管理します。DynamoDB はリクエストイベントの検索用データと 30 日間のリクエストログに使用し、ウォレットの別の正本にはしません。 - 仮押さえと送信処理の取得記録を永続化した後、上流へ直接送信します。上流のタスク ID を取得してから非同期タスクを返します。タスクの GET がポーリングと利用額の確定を行い、定期的な照合プロセスが未照会のタスクを確認してログ書き込みの失敗を再試行します。 - デフォルトのログにはメタデータのみを保存し、30 日後に失効させます。プロンプト、会話本文、入力ファイル、生成ファイル、Authorization ヘッダー、決済シークレットをこれらのログに保存してはいけません。 - 台帳と決済記録には独立した保存ポリシーを適用します。リクエストのメタデータを削除しても、財務履歴は削除しません。本番環境の保存要件は公開前に確定する必要があります。 - 照合待ちの滞留、長時間残る仮押さえ、重複した確定処理の試行、タスクの遅延、ログ再試行の滞留、上流エラー、決済金額の不一致を監視し、アラートを設定します。 公開前に認証情報、DNS/TLS、ワークロード権限を設定し、実際の連携を検証してください。 [タスク](/docs?page=tasks&lang=ja) · [残高と課金](/docs?page=billing&lang=ja) タスクの調整、財務トランザクション、ログ再試行の待機記録は MySQL で管理します。メタデータは DynamoDB に直接書き込み、アカウント・キー・時刻の検索用データ、日付バケット、LOG_SHARDS による分割を使用します。検索時には期限切れの記録を除外します。軽量な照合プロセスが未照会のタスクをポーリングし、ログ書き込みを再試行します。Web、gateway、reconciler は個別にデプロイします. 成功応答は生成結果の元の URL を変更せず返します。有効期限、可用性、クロスオリジン制限は元のホストが管理します。期限前に結果を保存してください。完了タスクの再取得では URL は延長されません。復旧用のメタデータと URL は保持する場合がありますが、生成ファイルは保存しません。 --- # 参考 URL と一時入力 モデルのメタデータが `image` に対応している場合、画像または動画のリクエストに、トップレベルの HTTPS 参考画像 URL を一つ指定できます。ブラウザの `blob:` URL にはサービスからアクセスできません。現在の正規化されたリクエストでは、複数の参考画像に対応していません。 参考画像に対応するモデルでは、公開 HTTPS URL を `image` に指定します。公開推論 API は URL を受け取り、ファイル本体は受け取りません。Playground では、ログイン済みかつ検証済みの利用者だけが一時的な画像や動画を非公開 R2 にアップロードできます。生成結果のダウンロード、保存、再ホストは行いません。 一時入力は PNG/JPEG/WebP が 10 MiB まで、MP4/WebM が 50 MiB までです。アップロード試行はアカウントごとに毎分 20 回までです。読み取り URL は最大 1 時間有効で、延長できません。オブジェクトは 1 日の有効期限ルールで非同期に削除され、24 時間ちょうどの物理削除は保証しません。画像 URL は対応する参考画像欄に使用できます。動画 URL は動画入力に対応するモデル専用で、参考画像としては使えません。外部の入力 URL は処理中にアクセスできる状態にしてください。 ```json {"model":"seedream-4.5","prompt":"A small blue house","image":"https://example.com/reference.png","n":1,"size":"2048x2048"} ``` 成功応答は生成結果の元の URL を変更せず返します。有効期限、可用性、クロスオリジン制限は元のホストが管理します。期限前に結果を保存してください。完了タスクの再取得では URL は延長されません。復旧用のメタデータと URL は保持する場合がありますが、生成ファイルは保存しません。 --- # Python 連携ガイド サンプルはサーバー側で実行してください。プラットフォーム専用 SDK ではなく、各言語の HTTP クライアントを使用します。実行コマンドを実行するまでリクエストは送信されません。モデルは事前の設定と有効化が必要です。 ## 環境の準備 実行環境: **Python 3.11+**. [完全なソースをダウンロード](/examples/python.py). デプロイ先の URL とサーバー用 API Key を環境変数に設定します。URL の末尾に `/v1` を一度だけ含めてください。`https://xxxrouter.com` は正式なサービスドメインです。ローカルテストではご自身のデプロイ先のオリジンに置き換えてください。現時点では未デプロイで、連携の認証も未完了です。 ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 呼び出し方法を選択 `chat` は SSE の差分と最終使用量を読み取ります。`image` は同期画像を受け取り、`async-image` と `video` は HTTP 202 のタスクを照会します。ポーリングは約 5 分(処理中の読み取り時間を除く)で停止します。タイムアウト後もタスク ID を保持し、後から照会できます。 ```sh python3 python.py chat # python3 python.py image # python3 python.py async-image # python3 python.py video ``` ## 冪等性とエラー復旧 通常、再実行すると新しい冪等キーが作成され、新たな料金が発生する可能性があります。アプリケーションでは送信前にキーと完全なリクエスト本文を永続化してください。`XXXROUTER_REQUEST_ID` に元の値を設定できるのは、モデル・本文・API Key・呼び出し元がすべて同じ論理リクエストを再開する場合だけです。通信エラー後に新しいキーで再送しないでください。各クライアントは生成 POST を一度だけ送信し、自動再試行しません。 レスポンスヘッダーの `X-Request-ID` を保存してください。HTTP エラー時は診断用に本文を保持します。`status=unknown` のタスクは自動ポーリングを停止し、後の照会または照合を待ちます。成功や無料利用とは見なしません。ストリーム内のエラーまたは `[DONE]` の欠落は中断を示します。使用量の欠落は料金ゼロを意味しません。接続を切断しても受理済みの処理は取り消されません。API Key や署名付きメディア URL をログに残さないでください。 ## 参照画像と結果ファイル 参考画像に対応するモデルでは、公開 HTTPS URL を `image` に指定します。公開推論 API は URL を受け取り、ファイル本体は受け取りません。Playground では、ログイン済みかつ検証済みの利用者だけが一時的な画像や動画を非公開 R2 にアップロードできます。生成結果のダウンロード、保存、再ホストは行いません。 成功応答は生成結果の元の URL を変更せず返します。有効期限、可用性、クロスオリジン制限は元のホストが管理します。期限前に結果を保存してください。完了タスクの再取得では URL は延長されません。復旧用のメタデータと URL は保持する場合がありますが、生成ファイルは保存しません。 ## 完全なサンプル このコードは連携例であり、実際のモデルや決済チャネルが有効であることを保証しません。ブラウザ用バンドルには含めないでください。 ```python """Python 3.11+, standard library only. No automatic generation retries.""" import json import os import sys import time import urllib.error import urllib.request import uuid BASE = os.environ['XXXROUTER_BASE_URL'].rstrip('/') # Include /v1 exactly once. KEY = os.environ['XXXROUTER_API_KEY'] MODE = sys.argv[1] if len(sys.argv) > 1 else 'chat' CASES = { 'chat': ('chat/completions', {'model': 'deepseek-v4-flash', 'messages': [{'role': 'user', 'content': 'Explain HTTP in one sentence.'}], 'max_tokens': 256, 'stream': True, 'stream_options': {'include_usage': True}}), 'image': ('images/generations', {'model': 'seedream-4.5', 'prompt': 'A ceramic cup in natural light', 'size': '2048x2048', 'n': 1}), 'async-image': ('images/generations', {'model': 'nano-banana-pro', 'prompt': 'A ceramic cup in natural light', 'size': '1024x1024', 'n': 1}), 'video': ('videos/generations', {'model': 'kling-3.0', 'prompt': 'A slow camera move across a mountain landscape', 'duration': 5, 'aspect_ratio': '16:9'}), } class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None opener = urllib.request.build_opener(NoRedirect) def request(path, payload=None, idem=None): headers = {'Authorization': 'Bearer ' + KEY} if idem: headers['Idempotency-Key'] = idem data = None if payload is not None: headers['Content-Type'] = 'application/json' data = json.dumps(payload).encode() req = urllib.request.Request(BASE + '/' + path, data=data, headers=headers) try: return opener.open(req, timeout=300 if payload is not None else 30) except urllib.error.HTTPError as error: # Read the structured failure: an uncertain request may already have a hold. print('HTTP', error.code, 'request_id=', error.headers.get('X-Request-ID'), file=sys.stderr) print(error.read().decode(), file=sys.stderr) raise def stream(response): event = [] def consume(): data = '\n'.join(event) if data == '[DONE]': return True if data: chunk = json.loads(data) if chunk.get('error'): raise RuntimeError(chunk['error']) for choice in chunk.get('choices', []): print(choice.get('delta', {}).get('content') or '', end='', flush=True) if chunk.get('usage'): print('\nusage:', chunk['usage']) return False for raw in response: line = raw.decode('utf-8').rstrip('\r\n') if line == '': if consume(): return event.clear() elif line.startswith('data:'): event.append(line[5:].lstrip(' ')) if event and consume(): return raise RuntimeError('Stream interrupted. Keep request ID and idempotency key; do not create another submission.') def finish(result): deadline = time.monotonic() + 300 while result.get('task_id') and result.get('status') not in ('succeeded', 'failed', 'unknown'): if time.monotonic() >= deadline: print(result) raise TimeoutError('Keep task_id and query later; do not resubmit.') time.sleep(2) with request('tasks/' + result['task_id']) as response: result = json.load(response) print(json.dumps(result, indent=2)) if result.get('status') == 'unknown': raise RuntimeError('Outcome unconfirmed. Retain IDs and wait for reconciliation; do not resubmit.') if result.get('status') == 'failed': raise RuntimeError('Generation failed. Inspect error and billing before a new logical request.') if __name__ == '__main__': path, payload = CASES[MODE] idem = os.environ.get('XXXROUTER_REQUEST_ID') or str(uuid.uuid4()) print('idempotency_key=', idem, file=sys.stderr) # Persist the key with this exact payload before submitting in a real application. with request(path, payload, idem) as response: print('request_id=', response.headers.get('X-Request-ID'), file=sys.stderr) if 'text/event-stream' in response.headers.get('Content-Type', ''): stream(response) else: finish(json.load(response)) ``` ## 関連ドキュメント [chat](/docs?page=chat&lang=ja) · [images](/docs?page=images&lang=ja) · [videos](/docs?page=videos&lang=ja) · [tasks](/docs?page=tasks&lang=ja) · [uploads](/docs?page=uploads&lang=ja) · [errors](/docs?page=errors&lang=ja) [言語の公式ドキュメント](https://docs.python.org/3/library/urllib.request.html) · 2026-10-26 --- # TypeScript / Node.js 連携ガイド サンプルはサーバー側で実行してください。プラットフォーム専用 SDK ではなく、各言語の HTTP クライアントを使用します。実行コマンドを実行するまでリクエストは送信されません。モデルは事前の設定と有効化が必要です。 ## 環境の準備 実行環境: **Node.js 22.18+**. [完全なソースをダウンロード](/examples/typescript.ts). デプロイ先の URL とサーバー用 API Key を環境変数に設定します。URL の末尾に `/v1` を一度だけ含めてください。`https://xxxrouter.com` は正式なサービスドメインです。ローカルテストではご自身のデプロイ先のオリジンに置き換えてください。現時点では未デプロイで、連携の認証も未完了です。 ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 呼び出し方法を選択 `chat` は SSE の差分と最終使用量を読み取ります。`image` は同期画像を受け取り、`async-image` と `video` は HTTP 202 のタスクを照会します。ポーリングは約 5 分(処理中の読み取り時間を除く)で停止します。タイムアウト後もタスク ID を保持し、後から照会できます。 ```sh node --experimental-strip-types typescript.ts chat # node --experimental-strip-types typescript.ts image # node --experimental-strip-types typescript.ts async-image # node --experimental-strip-types typescript.ts video ``` ## 冪等性とエラー復旧 通常、再実行すると新しい冪等キーが作成され、新たな料金が発生する可能性があります。アプリケーションでは送信前にキーと完全なリクエスト本文を永続化してください。`XXXROUTER_REQUEST_ID` に元の値を設定できるのは、モデル・本文・API Key・呼び出し元がすべて同じ論理リクエストを再開する場合だけです。通信エラー後に新しいキーで再送しないでください。各クライアントは生成 POST を一度だけ送信し、自動再試行しません。 レスポンスヘッダーの `X-Request-ID` を保存してください。HTTP エラー時は診断用に本文を保持します。`status=unknown` のタスクは自動ポーリングを停止し、後の照会または照合を待ちます。成功や無料利用とは見なしません。ストリーム内のエラーまたは `[DONE]` の欠落は中断を示します。使用量の欠落は料金ゼロを意味しません。接続を切断しても受理済みの処理は取り消されません。API Key や署名付きメディア URL をログに残さないでください。 ## 参照画像と結果ファイル 参考画像に対応するモデルでは、公開 HTTPS URL を `image` に指定します。公開推論 API は URL を受け取り、ファイル本体は受け取りません。Playground では、ログイン済みかつ検証済みの利用者だけが一時的な画像や動画を非公開 R2 にアップロードできます。生成結果のダウンロード、保存、再ホストは行いません。 成功応答は生成結果の元の URL を変更せず返します。有効期限、可用性、クロスオリジン制限は元のホストが管理します。期限前に結果を保存してください。完了タスクの再取得では URL は延長されません。復旧用のメタデータと URL は保持する場合がありますが、生成ファイルは保存しません。 ## 完全なサンプル このコードは連携例であり、実際のモデルや決済チャネルが有効であることを保証しません。ブラウザ用バンドルには含めないでください。 ```typescript // Node.js 22.18+; run: node --experimental-strip-types typescript.ts chat // Uses native fetch, including explicit HTTP and SSE error handling. import { randomUUID } from 'node:crypto'; type Result = { status?: string; task_id?: string; error?: unknown; [key: string]: any }; const base = process.env.XXXROUTER_BASE_URL?.replace(/\/$/, ''); // Include /v1. const key = process.env.XXXROUTER_API_KEY; if (!base || !key) throw new Error('Set XXXROUTER_BASE_URL and XXXROUTER_API_KEY'); const cases: Record]> = { chat: ['chat/completions', { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'Explain HTTP in one sentence.' }], max_tokens: 256, stream: true, stream_options: { include_usage: true } }], image: ['images/generations', { model: 'seedream-4.5', prompt: 'A ceramic cup in natural light', size: '2048x2048', n: 1 }], 'async-image': ['images/generations', { model: 'nano-banana-pro', prompt: 'A ceramic cup in natural light', size: '1024x1024', n: 1 }], video: ['videos/generations', { model: 'kling-3.0', prompt: 'A slow camera move across a mountain landscape', duration: 5, aspect_ratio: '16:9' }], }; async function request(path: string, payload?: unknown, idem?: string): Promise { const headers: Record = { Authorization: `Bearer ${key}` }; if (idem) headers['Idempotency-Key'] = idem; if (payload) headers['Content-Type'] = 'application/json'; const response = await fetch(`${base}/${path}`, { method: payload ? 'POST' : 'GET', headers, redirect: 'error', body: payload ? JSON.stringify(payload) : undefined, signal: AbortSignal.timeout(payload ? 300_000 : 30_000), }); console.error('request_id=', response.headers.get('X-Request-ID')); if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); return response; } async function stream(response: Response): Promise { if (!response.body) throw new Error('Empty response body'); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ''; let data: string[] = []; const consume = (): boolean => { const value = data.join('\n'); data = []; if (value === '[DONE]') return true; if (!value) return false; const chunk = JSON.parse(value); if (chunk.error) throw new Error(JSON.stringify(chunk.error)); for (const choice of chunk.choices ?? []) process.stdout.write(choice.delta?.content ?? ''); if (chunk.usage) console.log('\nusage:', chunk.usage); return false; }; try { while (true) { const { value, done } = await reader.read(); buffer += value ?? ''; let end: number; while ((end = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, end).replace(/\r$/, ''); buffer = buffer.slice(end + 1); if (!line && consume()) return; if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, '')); } if (done) break; } if (buffer.startsWith('data:')) data.push(buffer.slice(5).trimStart()); if (consume()) return; throw new Error('Stream interrupted. Keep request ID and idempotency key; do not resubmit.'); } finally { await reader.cancel(); reader.releaseLock(); } } async function finish(result: Result): Promise { const deadline = Date.now() + 300_000; while (result.task_id && !['succeeded', 'failed', 'unknown'].includes(result.status ?? '')) { if (Date.now() >= deadline) throw new Error(`Query task ${result.task_id} later; do not resubmit`); await new Promise(resolve => setTimeout(resolve, 2000)); result = await (await request(`tasks/${encodeURIComponent(result.task_id)}`)).json(); } console.log(result); if (result.status === 'unknown') throw new Error('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (result.status === 'failed') throw new Error('Generation failed; inspect error and billing.'); } const [path, payload] = cases[process.argv[2] ?? 'chat']; const idem = process.env.XXXROUTER_REQUEST_ID || randomUUID(); console.error('idempotency_key=', idem); // Persist alongside the exact payload before submitting. const response = await request(path, payload, idem); if (response.headers.get('content-type')?.includes('text/event-stream')) await stream(response); else await finish(await response.json()); ``` ## 関連ドキュメント [chat](/docs?page=chat&lang=ja) · [images](/docs?page=images&lang=ja) · [videos](/docs?page=videos&lang=ja) · [tasks](/docs?page=tasks&lang=ja) · [uploads](/docs?page=uploads&lang=ja) · [errors](/docs?page=errors&lang=ja) [言語の公式ドキュメント](https://nodejs.org/api/globals.html#fetch) · 2026-10-26 --- # Go 連携ガイド サンプルはサーバー側で実行してください。プラットフォーム専用 SDK ではなく、各言語の HTTP クライアントを使用します。実行コマンドを実行するまでリクエストは送信されません。モデルは事前の設定と有効化が必要です。 ## 環境の準備 実行環境: **Go 1.22+**. [完全なソースをダウンロード](/examples/go.go). デプロイ先の URL とサーバー用 API Key を環境変数に設定します。URL の末尾に `/v1` を一度だけ含めてください。`https://xxxrouter.com` は正式なサービスドメインです。ローカルテストではご自身のデプロイ先のオリジンに置き換えてください。現時点では未デプロイで、連携の認証も未完了です。 ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 呼び出し方法を選択 `chat` は SSE の差分と最終使用量を読み取ります。`image` は同期画像を受け取り、`async-image` と `video` は HTTP 202 のタスクを照会します。ポーリングは約 5 分(処理中の読み取り時間を除く)で停止します。タイムアウト後もタスク ID を保持し、後から照会できます。 ```sh go run go.go chat # go run go.go image # go run go.go async-image # go run go.go video ``` ## 冪等性とエラー復旧 通常、再実行すると新しい冪等キーが作成され、新たな料金が発生する可能性があります。アプリケーションでは送信前にキーと完全なリクエスト本文を永続化してください。`XXXROUTER_REQUEST_ID` に元の値を設定できるのは、モデル・本文・API Key・呼び出し元がすべて同じ論理リクエストを再開する場合だけです。通信エラー後に新しいキーで再送しないでください。各クライアントは生成 POST を一度だけ送信し、自動再試行しません。 レスポンスヘッダーの `X-Request-ID` を保存してください。HTTP エラー時は診断用に本文を保持します。`status=unknown` のタスクは自動ポーリングを停止し、後の照会または照合を待ちます。成功や無料利用とは見なしません。ストリーム内のエラーまたは `[DONE]` の欠落は中断を示します。使用量の欠落は料金ゼロを意味しません。接続を切断しても受理済みの処理は取り消されません。API Key や署名付きメディア URL をログに残さないでください。 ## 参照画像と結果ファイル 参考画像に対応するモデルでは、公開 HTTPS URL を `image` に指定します。公開推論 API は URL を受け取り、ファイル本体は受け取りません。Playground では、ログイン済みかつ検証済みの利用者だけが一時的な画像や動画を非公開 R2 にアップロードできます。生成結果のダウンロード、保存、再ホストは行いません。 成功応答は生成結果の元の URL を変更せず返します。有効期限、可用性、クロスオリジン制限は元のホストが管理します。期限前に結果を保存してください。完了タスクの再取得では URL は延長されません。復旧用のメタデータと URL は保持する場合がありますが、生成ファイルは保存しません。 ## 完全なサンプル このコードは連携例であり、実際のモデルや決済チャネルが有効であることを保証しません。ブラウザ用バンドルには含めないでください。 ```go // Go 1.22+, standard library only. Run: go run go.go chat package main import ( "bufio" "bytes" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) type Object = map[string]any var base = strings.TrimRight(os.Getenv("XXXROUTER_BASE_URL"), "/") // Include /v1. var key = os.Getenv("XXXROUTER_API_KEY") var client = &http.Client{Timeout: 300 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} func request(path string, payload Object, idem string) (*http.Response, error) { var body io.Reader method := "GET" if payload != nil { raw, err := json.Marshal(payload) if err != nil { return nil, err } body = bytes.NewReader(raw) method = "POST" } req, err := http.NewRequest(method, base+"/"+path, body) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) if payload != nil { req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", idem) } response, err := client.Do(req) if err != nil { return nil, err } fmt.Fprintln(os.Stderr, "request_id=", response.Header.Get("X-Request-ID")) if response.StatusCode < 200 || response.StatusCode >= 300 { defer response.Body.Close() raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20)) return nil, fmt.Errorf("HTTP %d: %s", response.StatusCode, raw) } return response, nil } func decode(response *http.Response) (Object, error) { defer response.Body.Close() var result Object err := json.NewDecoder(response.Body).Decode(&result) return result, err } func stream(response *http.Response) error { defer response.Body.Close() scanner := bufio.NewScanner(response.Body) scanner.Buffer(make([]byte, 4096), 2<<20) var parts []string consume := func() (bool, error) { data := strings.Join(parts, "\n") parts = nil if data == "[DONE]" { return true, nil } if data == "" { return false, nil } var chunk struct { Error json.RawMessage `json:"error"` Usage json.RawMessage `json:"usage"` Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` } if err := json.Unmarshal([]byte(data), &chunk); err != nil { return false, err } if len(chunk.Error) > 0 && string(chunk.Error) != "null" { return false, fmt.Errorf("stream error: %s", chunk.Error) } for _, choice := range chunk.Choices { fmt.Print(choice.Delta.Content) } if len(chunk.Usage) > 0 && string(chunk.Usage) != "null" { fmt.Printf("\nusage: %s\n", chunk.Usage) } return false, nil } for scanner.Scan() { line := scanner.Text() if line == "" { done, err := consume() if err != nil || done { return err } } if strings.HasPrefix(line, "data:") { parts = append(parts, strings.TrimPrefix(line[5:], " ")) } } if err := scanner.Err(); err != nil { return err } done, err := consume() if err != nil || done { return err } return fmt.Errorf("stream interrupted; keep request ID and idempotency key, do not resubmit") } func finish(result Object) error { deadline := time.Now().Add(5 * time.Minute) for { task, hasTask := result["task_id"].(string) status, _ := result["status"].(string) if !hasTask || status == "succeeded" || status == "failed" || status == "unknown" { break } if time.Now().After(deadline) { return fmt.Errorf("query task %s later; do not resubmit", task) } time.Sleep(2 * time.Second) response, err := request("tasks/"+task, nil, "") if err != nil { return err } result, err = decode(response) if err != nil { return err } } raw, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(raw)) if result["status"] == "unknown" { return fmt.Errorf("outcome unconfirmed; retain IDs and wait for reconciliation") } if result["status"] == "failed" { return fmt.Errorf("generation failed; inspect error and billing") } return nil } func run() error { if base == "" || key == "" { return fmt.Errorf("set XXXROUTER_BASE_URL and XXXROUTER_API_KEY") } mode := "chat" if len(os.Args) > 1 { mode = os.Args[1] } path := "images/generations" var payload Object switch mode { case "chat": path = "chat/completions" payload = Object{"model": "deepseek-v4-flash", "messages": []Object{{"role": "user", "content": "Explain HTTP in one sentence."}}, "max_tokens": 256, "stream": true, "stream_options": Object{"include_usage": true}} case "image": payload = Object{"model": "seedream-4.5", "prompt": "A ceramic cup in natural light", "size": "2048x2048", "n": 1} case "async-image": payload = Object{"model": "nano-banana-pro", "prompt": "A ceramic cup in natural light", "size": "1024x1024", "n": 1} case "video": path = "videos/generations" payload = Object{"model": "kling-3.0", "prompt": "A slow camera move across a mountain landscape", "duration": 5, "aspect_ratio": "16:9"} default: return fmt.Errorf("choose chat, image, async-image or video") } idem := os.Getenv("XXXROUTER_REQUEST_ID") if idem == "" { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return err } idem = hex.EncodeToString(b) } fmt.Fprintln(os.Stderr, "idempotency_key=", idem) // Persist with the exact payload before submission. response, err := request(path, payload, idem) if err != nil { return err } if strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { return stream(response) } result, err := decode(response) if err != nil { return err } return finish(result) } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } ``` ## 関連ドキュメント [chat](/docs?page=chat&lang=ja) · [images](/docs?page=images&lang=ja) · [videos](/docs?page=videos&lang=ja) · [tasks](/docs?page=tasks&lang=ja) · [uploads](/docs?page=uploads&lang=ja) · [errors](/docs?page=errors&lang=ja) [言語の公式ドキュメント](https://pkg.go.dev/net/http) · 2026-10-26 --- # PHP 連携ガイド サンプルはサーバー側で実行してください。プラットフォーム専用 SDK ではなく、各言語の HTTP クライアントを使用します。実行コマンドを実行するまでリクエストは送信されません。モデルは事前の設定と有効化が必要です。 ## 環境の準備 実行環境: **PHP 8.1+ + ext-curl**. [完全なソースをダウンロード](/examples/php.php). デプロイ先の URL とサーバー用 API Key を環境変数に設定します。URL の末尾に `/v1` を一度だけ含めてください。`https://xxxrouter.com` は正式なサービスドメインです。ローカルテストではご自身のデプロイ先のオリジンに置き換えてください。現時点では未デプロイで、連携の認証も未完了です。 ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 呼び出し方法を選択 `chat` は SSE の差分と最終使用量を読み取ります。`image` は同期画像を受け取り、`async-image` と `video` は HTTP 202 のタスクを照会します。ポーリングは約 5 分(処理中の読み取り時間を除く)で停止します。タイムアウト後もタスク ID を保持し、後から照会できます。 ```sh php php.php chat # php php.php image # php php.php async-image # php php.php video ``` ## 冪等性とエラー復旧 通常、再実行すると新しい冪等キーが作成され、新たな料金が発生する可能性があります。アプリケーションでは送信前にキーと完全なリクエスト本文を永続化してください。`XXXROUTER_REQUEST_ID` に元の値を設定できるのは、モデル・本文・API Key・呼び出し元がすべて同じ論理リクエストを再開する場合だけです。通信エラー後に新しいキーで再送しないでください。各クライアントは生成 POST を一度だけ送信し、自動再試行しません。 レスポンスヘッダーの `X-Request-ID` を保存してください。HTTP エラー時は診断用に本文を保持します。`status=unknown` のタスクは自動ポーリングを停止し、後の照会または照合を待ちます。成功や無料利用とは見なしません。ストリーム内のエラーまたは `[DONE]` の欠落は中断を示します。使用量の欠落は料金ゼロを意味しません。接続を切断しても受理済みの処理は取り消されません。API Key や署名付きメディア URL をログに残さないでください。 ## 参照画像と結果ファイル 参考画像に対応するモデルでは、公開 HTTPS URL を `image` に指定します。公開推論 API は URL を受け取り、ファイル本体は受け取りません。Playground では、ログイン済みかつ検証済みの利用者だけが一時的な画像や動画を非公開 R2 にアップロードできます。生成結果のダウンロード、保存、再ホストは行いません。 成功応答は生成結果の元の URL を変更せず返します。有効期限、可用性、クロスオリジン制限は元のホストが管理します。期限前に結果を保存してください。完了タスクの再取得では URL は延長されません。復旧用のメタデータと URL は保持する場合がありますが、生成ファイルは保存しません。 ## 完全なサンプル このコードは連携例であり、実際のモデルや決済チャネルが有効であることを保証しません。ブラウザ用バンドルには含めないでください。 ```php getMessage(); } }; $curl = curl_init($base . '/' . $path); $options = [ CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $payload !== null ? 300 : 30, CURLOPT_HEADERFUNCTION => function ($handle, string $line) use (&$contentType, &$requestId): int { if (stripos($line, 'Content-Type:') === 0) $contentType = trim(substr($line, 13)); if (stripos($line, 'X-Request-ID:') === 0) $requestId = trim(substr($line, 13)); return strlen($line); }, CURLOPT_WRITEFUNCTION => function ($handle, string $bytes) use (&$contentType, &$raw, &$buffer, &$data, &$done, &$streamError, $consume): int { if (!str_contains($contentType, 'text/event-stream')) { $raw .= $bytes; return strlen($bytes); } $buffer .= $bytes; while (($end = strpos($buffer, "\n")) !== false) { $line = rtrim(substr($buffer, 0, $end), "\r"); $buffer = substr($buffer, $end + 1); if ($line === '') $consume(); elseif (str_starts_with($line, 'data:')) $data[] = ltrim(substr($line, 5), ' '); } return strlen($bytes); }, ]; if ($payload !== null) { $headers[] = 'Content-Type: application/json'; $headers[] = 'Idempotency-Key: ' . $idem; $options[CURLOPT_POST] = true; $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_THROW_ON_ERROR); } $options[CURLOPT_HTTPHEADER] = $headers; curl_setopt_array($curl, $options); try { $ok = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); fwrite(STDERR, 'request_id=' . $requestId . "\n"); if ($ok === false) throw new RuntimeException('Transport error; retain IDs, do not resubmit: ' . curl_error($curl)); if ($status < 200 || $status >= 300) throw new RuntimeException('HTTP ' . $status . ': ' . $raw); if (str_contains($contentType, 'text/event-stream')) { if (str_starts_with($buffer, 'data:')) $data[] = ltrim(substr($buffer, 5), ' '); $consume(); if ($streamError !== null) throw new RuntimeException($streamError); if (!$done) throw new RuntimeException('Stream interrupted; retain request ID and idempotency key.'); return ['stream_complete' => true]; } return json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } finally { curl_close($curl); } } $cases = [ 'chat' => ['chat/completions', ['model'=>'deepseek-v4-flash','messages'=>[['role'=>'user','content'=>'Explain HTTP in one sentence.']],'max_tokens'=>256,'stream'=>true,'stream_options'=>['include_usage'=>true]]], 'image' => ['images/generations', ['model'=>'seedream-4.5','prompt'=>'A ceramic cup in natural light','size'=>'2048x2048','n'=>1]], 'async-image' => ['images/generations', ['model'=>'nano-banana-pro','prompt'=>'A ceramic cup in natural light','size'=>'1024x1024','n'=>1]], 'video' => ['videos/generations', ['model'=>'kling-3.0','prompt'=>'A slow camera move across a mountain landscape','duration'=>5,'aspect_ratio'=>'16:9']], ]; $mode = $argv[1] ?? 'chat'; if (!isset($cases[$mode])) throw new InvalidArgumentException('Choose chat, image, async-image or video'); [$path, $payload] = $cases[$mode]; $idem = getenv('XXXROUTER_REQUEST_ID') ?: bin2hex(random_bytes(16)); fwrite(STDERR, 'idempotency_key=' . $idem . "\n"); // Persist alongside exact payload before submission. $result = request($path, $payload, $idem); $deadline = microtime(true) + 300; while (isset($result['task_id']) && !in_array($result['status'], ['succeeded', 'failed', 'unknown'], true)) { if (microtime(true) >= $deadline) throw new RuntimeException('Query task ' . $result['task_id'] . ' later; do not resubmit.'); sleep(2); $result = request('tasks/' . rawurlencode($result['task_id'])); } echo json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . "\n"; if (($result['status'] ?? '') === 'unknown') throw new RuntimeException('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (($result['status'] ?? '') === 'failed') throw new RuntimeException('Generation failed; inspect error and billing.'); ``` ## 関連ドキュメント [chat](/docs?page=chat&lang=ja) · [images](/docs?page=images&lang=ja) · [videos](/docs?page=videos&lang=ja) · [tasks](/docs?page=tasks&lang=ja) · [uploads](/docs?page=uploads&lang=ja) · [errors](/docs?page=errors&lang=ja) [言語の公式ドキュメント](https://www.php.net/manual/en/function.curl-exec.php) · 2026-10-26 --- # 更新履歴 ## 0.1.0 — 2026-10-26 — 実装草案 - アカウントの USD 残高と、キーごとの利用額制御を追加しました。 - en / zh-CN の開発者リファレンスと、機械可読の OpenAPI 草案を追加しました。 - 結果を直接返す同期画像生成と、非同期の画像・動画タスクを区別しました。 - 任意で SSE を使用できる OpenAI 形式のテキストチャットを定義しました。 - 有効期間 7 日間の $0.20 試用クレジットと、Payblis / NOWPayments のホスト型チェックアウトを追加しました。 - デフォルトのリクエストログをメタデータのみに限定し、保存期間を 30 日間に設定しました。 本リファレンスは、実装済みのエンドポイントを説明します。デプロイ、実際のルートの動作検証、決済チャネルの受け入れ確認は、引き続き別途必要なリリース要件です。 [クイックスタート](/docs?page=quickstart&lang=ja) --- # 빠른 시작 > 구현 참조 문서 · 2026-10-26 · 트래픽을 보내기 전에 배포 환경을 구성하세요. 이 애플리케이션은 서버에서 인증, 잔액, 요청을 관리합니다. `https://xxxrouter.com`은 공식 서비스 도메인입니다. 로컬 테스트에서는 자신의 배포 환경 origin으로 바꾸세요. 현재 서비스는 아직 배포되지 않았으며 연동 검증도 완료되지 않았습니다. 게이트웨이를 통해 동일 출처의 `/v1`도 지원합니다. 모델은 기본적으로 비활성화되어 있습니다. 운영자는 모델을 활성화하기 전에 자격 증명을 구성하고, 경로를 검증하고, 가격을 설정해야 합니다. 이 문서만으로 배포나 통합 테스트가 완료되었다고 판단해서는 안 됩니다. 1. 콘솔에 로그인하고 USD 잔액을 확인합니다. 2. API key를 생성합니다. 한 번 표시되는 키를 복사하여 서버의 시크릿 관리자에 저장합니다. 3. 모델의 응답 방식과 적용 가격을 확인합니다. 4. 백엔드에서 API를 호출합니다. 반환된 요청 ID를 사용량 및 로그에서 추적합니다. ```sh export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' curl https://xxxrouter.com/v1/models \ -H "Authorization: Bearer $XXXROUTER_API_KEY" ``` ```sh curl https://xxxrouter.com/v1/chat/completions \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-chat-request-001' \ -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Hello"}],"max_tokens":256,"stream":false}' ``` 채팅은 OpenAI 스타일의 계약을 따릅니다. 이미지 API는 직접 결과를 반환하는 방식과 작업을 반환하는 방식을 모두 지원하며, 모든 모델이 OpenAI Images를 그대로 대체할 수 있는 것은 아닙니다. 콘솔 Playground는 로그인한 세션을 통해 외부 API 호출과 동일한 가격 및 회계 정책을 사용하며, 사용량은 API key와 별도로 구분합니다. 다음 단계: [인증](/docs?page=auth&lang=ko), [모델 기능](/docs?page=models&lang=ko), [이미지](/docs?page=images&lang=ko), [오류](/docs?page=errors&lang=ko). 기계 판독용 인터페이스: [OpenAPI](/openapi.json), [llms.txt](/llms.txt), [전체 텍스트](/llms-full.txt). --- # 인증 생성, 작업, 키 조회 요청에는 `Authorization: Bearer `를 전송하세요. 모델 카탈로그 GET 엔드포인트는 인증 없이 공개 가격을 조회할 수 있으며, 유효한 키를 선택적으로 전달하면 계정 가격이 적용됩니다. 키를 쿼리 문자열, 공개 저장소, 클라이언트 번들, 공유 스크린샷 또는 브라우저 저장소에 넣지 마세요. 인증 및 이메일 확인을 마친 콘솔 계정에서 실제 키를 생성하세요. 실제 키는 생성 시 한 번만 표시됩니다. 이후에는 접두사/접미사와 변경할 수 없는 ID만 표시됩니다. 서버는 복구 가능한 평문 대신 키 기반 단방향 다이제스트를 저장합니다. 키 관리에는 콘솔 세션이 필요합니다. 추론용 키로 다른 키를 생성하거나 계정 결제 설정을 변경할 수 없습니다. 키를 교체하려면 새 키를 생성하고 배포한 뒤 트래픽이 정상적으로 처리되는지 확인하고 기존 키를 폐기하세요. 일시 중지는 되돌릴 수 있지만 폐기는 영구적입니다. 만료 여부는 새 요청마다 UTC 기준으로 평가합니다. 이미 수락된 작업은 키가 일시 중지, 만료 또는 폐기되어도 과금 정보와 이력을 유지합니다. 같은 계정의 다른 승인된 키나 콘솔을 사용하여 해당 작업을 조회하세요. `GET /v1/key`는 현재 키의 메타데이터를 반환하며, 적용 중인 제한과 남은 사용 한도를 포함합니다. 비밀 키 자체는 반환하지 않습니다. 오류 응답은 추측한 키가 다른 계정에 속하는지 여부를 노출하지 않습니다. [키 관리](/docs?page=keys&lang=ko) · [오류](/docs?page=errors&lang=ko) --- # 모델 및 가격 `GET /v1/models`는 계정에서 사용할 수 있는 모델을 나열합니다. `GET /v1/models/{id}`는 기능, 매개변수, 응답 방식, 가용 상태 및 적용 가격을 반환합니다. 공개 모델 ID는 라우팅 접두사가 없는 플랫폼 소유 이름입니다. 예를 들면 `/v1/models/seedream-4.5`입니다. 라우팅 및 인프라 메타데이터는 내부 정보이며 공개 모델 API에서 반환하지 않습니다. | 데모 모델 ID | 출력 | 응답 방식 | |---|---|---| | `seedream-4.5` | 이미지 | `sync` | | `nano-banana-pro` | 이미지 | `async` | | `flux-2-pro` | 이미지 | `async` | | `kling-3.0` | 동영상 | `async` | | `deepseek-v4-flash` | 텍스트 | 카탈로그 모드는 `stream`; `stream`으로 JSON 또는 SSE 선택 | 이는 플랫폼의 공개 모델 ID이며, 실제 서비스 경로가 구성되거나 검증되어 사용 가능하다는 보장은 아닙니다. 모델의 응답 방식은 카탈로그 메타데이터에 명시되며, URL의 일부 문자열이나 프로그래밍 언어의 `async` 키워드로 추론해서는 안 됩니다. 모델 기능 검증은 금액 사전 확보나 공급자 요청 제출 전에 이루어집니다. 가격은 USD 기준입니다. 이미지 요금은 지원 매개변수에 따라 달라지고, 이번 릴리스의 동영상 요금은 길이에 따라 달라지며, LLM은 입력 토큰과 출력 토큰에 별도의 요금을 적용합니다. 요청을 수락할 때 적용 가격 버전을 저장하므로 이후 카탈로그를 수정해도 해당 요청의 가격은 바뀌지 않습니다. 예상 비용은 확정된 정산 비용이 아닙니다. [이미지 프로토콜](/docs?page=images&lang=ko) · [과금](/docs?page=billing&lang=ko) ## 적용 가격의 우선순위 일치하는 규칙 하나만 사용합니다. 사용자 × 모델 재정의, 가격 그룹 × 모델 재정의, 가격 그룹 기본 할인, 공개 가격 순으로 적용합니다. 재정의는 고정 가격이거나 해당 모델의 허용 범위 내 할인일 수 있습니다. 규칙은 중첩되지 않습니다. 원가보다 낮은 가격은 운영자의 명시적 확인이 필요하며, 공급자 원가를 알 수 없을 때 임의의 마진을 표시해서는 안 됩니다. 요청 수락 시 선택한 규칙, 단위 및 버전을 고정합니다. 모델의 `parameters` 목록에는 정규화된 최상위 필드가 명시됩니다. 이미지는 `size`, `n`, `image`, 동영상은 `duration`, `aspect_ratio`, `image`, 채팅은 `temperature`, `max_tokens`, `stream`, `stream_options`를 사용합니다. 임의의 필드를 그대로 전달하는 사전 구조가 아닙니다. Seedream 4.5 예제는 `2048x2048`을 사용합니다. 프로덕션 매개변수 제한은 모델별로 검증해야 합니다. UI의 예제 선택지가 실제 기능을 보장하지는 않습니다. 모델 JSON은 name, modality (llm/image/video), mode/response_mode (stream/sync/async), parameters, sizes, supportsImage 및 enabled를 사용합니다. pricing은 currency, price 및 null을 허용하는 outputPrice를 포함하는 객체이며, 금액은 소수점 이하 여섯 자리의 USD 문자열입니다. price/outputPrice는 숫자형 USD 필드입니다. 비활성화된 모델은 호출할 수 없습니다. 초기 가격은 운영자의 검토가 필요하며, 알 수 없는 원가를 마진으로 표시하지 않습니다. --- # 채팅 완성 `POST /v1/chat/completions`는 `model`, `messages`, `max_tokens`, `temperature`, `stream` 및 선택 사항인 `stream_options: {"include_usage": true}`를 받습니다. v0.1 계약은 `system`, `user`, `assistant` 역할의 텍스트 메시지를 지원합니다. 도구 호출, 멀티모달 채팅, 임베딩 및 Responses API는 이번 릴리스 계약의 범위에 포함되지 않습니다. ```json {"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Explain HTTP in one sentence."}],"max_tokens":256,"temperature":0.7,"stream":true,"stream_options":{"include_usage":true}} ``` 비스트리밍 결과에는 `id`, `object: "chat.completion"`, `model`, `choices` 및 `usage`가 포함됩니다. 스트리밍은 `text/event-stream`을 반환하며, 각 data 이벤트에는 `choices[].delta`를 포함한 완성 청크가 들어 있습니다. `stream_options.include_usage=true`이면 상위 서비스가 최종 사용량 이벤트를 제공하는 경우 `data: [DONE]` 앞에 전달됩니다. 응답 헤더의 `X-Request-ID`를 기록하세요. SSE를 하나의 JSON 문서로 파싱하지 마세요. 게이트웨이는 JSON 전용 출력을 강제하는 시스템 프롬프트를 삽입하거나, 알리지 않고 모델을 바꾸거나, 출력을 수정하기 위해 과금된 완성 요청을 다시 실행해서는 안 됩니다. 연결을 끊거나 중지를 눌러도 이미 생성된 토큰은 취소되지 않습니다. 최종 과금은 관측된 사용량을 기준으로 합니다. 신뢰할 수 있는 사용량이 없으면 대사 대기 상태를 유지하며, 임의로 0으로 처리하지 않습니다. 각 경로는 활성화 전에 통합 검증을 거쳐야 합니다. [과금](/docs?page=billing&lang=ko) · [프로덕션 연동](/docs?page=production&lang=ko) 모든 생성 POST 요청에는 Idempotency-Key가 필요합니다(출력 가능한 ASCII 문자 1–128자). 새 논리 요청에는 새 값을 사용하고, 정확히 동일한 요청을 재전송할 때만 같은 값을 사용하세요. max_tokens 범위는 1–4096, temperature 범위는 0–2입니다. 스트리밍 요청을 재전송해도 과금된 상위 스트림을 다시 시작하거나 재생하지 않습니다. --- # 이미지 생성 `POST /v1/images/generations`에는 모델 카탈로그에서 선택하는 두 가지 명시적 응답 방식이 있습니다. 항상 모델 메타데이터를 확인하고 문서에 명시된 응답을 처리하세요. 두 방식 모두 `model`, `prompt`, 선택 사항인 최상위 `image` 참조 URL, `size`, `n`을 받습니다. 이는 정규화된 플랫폼 필드이며, 공급자 어댑터가 검증된 모델 스키마로 변환합니다. ```sh curl https://xxxrouter.com/v1/images/generations \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-image-request-001' \ -d '{"model":"seedream-4.5","prompt":"A ceramic cup in natural light","size":"2048x2048","n":1}' ``` ## 동기 모델: HTTP 200 ```json {"request_id":"req_demo_sync","model":"seedream-4.5","response_mode":"sync","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/cup.png"}],"billing":{"status":"settled","currency":"USD","amount":"0.030000"}} ``` 이미지는 최초 HTTP 응답으로 반환됩니다. 공급자 작업 ID나 후속 작업 폴링이 필요하지 않습니다. 위 금액은 데모이며 견적이 아닙니다. 동기 실행이 느려져도 동기 방식은 유지됩니다. 클라이언트 타임아웃은 실패의 증거나 재제출을 허용하는 근거가 아닙니다. ## 비동기 모델: HTTP 202 `nano-banana-pro` 또는 `flux-2-pro`를 선택하면 다음 응답을 받습니다. ```json {"request_id":"req_demo_async","task_id":"task_demo_01","model":"nano-banana-pro","response_mode":"async","status":"running"} ``` 최종 상태가 될 때까지 [작업 엔드포인트](/docs?page=tasks&lang=ko)를 폴링하세요. `202`를 출력 완료로 해석해서는 안 됩니다. 동일한 이미지 경로를 사용한다고 해서 모든 모델이 OpenAI Images와 호환되는 것은 아닙니다. 구성된 경로는 실제 요청을 제출합니다. 자격 증명이 없을 때 과금되는 모의 결과를 생성하지 않습니다. [참조 입력](/docs?page=uploads&lang=ko) · [오류](/docs?page=errors&lang=ko) 현재 서비스는 n=1만 지원합니다. image는 모델 parameters에 해당 필드가 포함된 경우에만 허용됩니다. 성공 응답은 생성 결과의 원본 URL을 변경 없이 반환합니다. 가용성, 만료 및 교차 출처 제한은 원본 호스트가 결정합니다. 만료 전에 결과를 저장하세요. 완료된 작업을 다시 조회해도 URL이 갱신되지 않습니다. 복구용 메타데이터와 URL은 보관할 수 있지만 생성 파일은 보관하지 않습니다. --- # 동영상 생성 `POST /v1/videos/generations`는 HTTP 202와 작업을 반환합니다. 공개 모델 ID는 `kling-3.0`이며, 실제 사용을 위해서는 경로가 구성되고 검증되어야 합니다. ```json {"model":"kling-3.0","prompt":"A slow camera movement around a ceramic cup","duration":5,"aspect_ratio":"16:9"} ``` 논리적 제출마다 `Idempotency-Key`를 전송하세요. `task_id`를 저장하고 `/v1/tasks/{task_id}`를 폴링하세요. 성공한 결과에는 동영상 URL, 미디어 유형 및 최종 과금 정보가 포함됩니다. 공급자가 실제로 지원하지 않는 한 취소 기능이나 공급자 진행률을 약속하지 마세요. 신뢰할 수 있는 근거가 없으면 정확한 진행률은 생략됩니다. 견적에 사용하는 길이는 가격 산정 입력값이며, 생성된 동영상이 정확히 그 길이라는 보장은 아닙니다. 출시 전에 각 경로의 출력 길이와 매개변수 조합별 과금 방식을 문서화해야 합니다. 다른 모델로의 자동 대체는 전제하지 않습니다. [작업](/docs?page=tasks&lang=ko) · [과금](/docs?page=billing&lang=ko) 정규화된 최상위 매개변수는 `duration`, `aspect_ratio` 및 선택 사항인 `image`입니다. 현재 스키마는 5/10초와 16:9, 9:16 또는 1:1을 허용합니다. 호출 전에 모델의 가용 상태를 확인하세요. 이 초안 요청에는 `resolution`이나 중첩된 `parameters` 필드가 없습니다. 성공 응답은 생성 결과의 원본 URL을 변경 없이 반환합니다. 가용성, 만료 및 교차 출처 제한은 원본 호스트가 결정합니다. 만료 전에 결과를 저장하세요. 완료된 작업을 다시 조회해도 URL이 갱신되지 않습니다. 복구용 메타데이터와 URL은 보관할 수 있지만 생성 파일은 보관하지 않습니다. --- # 작업 및 멱등성 `GET /v1/tasks/{id}`는 수락된 이미지/동영상 작업을 조회합니다. 조회할 때마다 인증과 계정 소유권을 확인합니다. 같은 계정의 다른 유효한 키로 작업을 조회할 수 있지만, 사용량의 귀속은 최초 키 ID로 고정되며 변경되지 않습니다. 상태: `submitting → running → succeeded | failed`. 결과가 불확실하면 요청은 `status: "unknown"`, 과금은 `billing.status: "reconciling"`을 사용하며, 자동으로 재제출해서는 안 됩니다. 작업 출력의 성공 여부와 과금 상태는 별개입니다. 정산을 대사하는 동안에도 결과가 제공될 수 있습니다. ```json {"task_id":"task_demo_01","request_id":"req_demo_async","model":"nano-banana-pro","response_mode":"async","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/image.png","media_type":"image"}],"billing":{"status":"settled","currency":"USD","amount":"0.150000"}} ``` 제한된 간격으로 폴링하세요(2초부터 시작하여 최대 10초까지 간격을 늘립니다). `Retry-After`를 준수하세요. GET을 재시도해도 새 작업은 생성되지 않습니다. POST 응답을 받지 못한 경우는 다릅니다. 동일한 멱등성 키와 정확히 같은 페이로드를 재사용하세요. 계정 + 엔드포인트 + 키 + 요청 지문이 동일하면 최초 레코드를 반환하며, 다른 페이로드는 `409 IDEMPOTENCY_CONFLICT`를 반환합니다. 멱등성 레코드는 MySQL에 영구 저장합니다. 날짜가 바뀌었다고 해서 이전 키를 재사용하거나 결과가 불확실한 요청을 재제출해도 안전하다고 가정해서는 안 됩니다. 동기 이미지 요청을 재전송하면 기록된 직접 결과가 있는 경우 해당 결과를 반환합니다. 진행 중이거나 결과가 불확실한 동기 요청은 현재 요청 레코드와 함께 HTTP `409`를 반환하며, 상위 서비스에 두 번째 요청을 제출하지 않습니다. 동기 요청 ID를 작업 ID처럼 `/tasks`에 전달하여 폴링해서는 안 됩니다. [오류](/docs?page=errors&lang=ko) · [프로덕션 연동](/docs?page=production&lang=ko) 성공 응답은 생성 결과의 원본 URL을 변경 없이 반환합니다. 가용성, 만료 및 교차 출처 제한은 원본 호스트가 결정합니다. 만료 전에 결과를 저장하세요. 완료된 작업을 다시 조회해도 URL이 갱신되지 않습니다. 복구용 메타데이터와 URL은 보관할 수 있지만 생성 파일은 보관하지 않습니다. --- # 잔액 및 과금 계정에는 하나의 선불 USD 잔액이 있습니다. 키별 사용 한도는 소비를 제한하는 장치이며 별도의 지갑이 아닙니다. 사용 가능 금액, 사전 확보 금액 및 정산 금액을 명확히 구분하여 표시하세요. 통화는 정수 마이크로달러나 정확한 십진수로 저장하며, 이진 부동소수점으로 저장하지 마세요. 승인된 체험 혜택은 7일 동안 유효한 $0.20 프로모션 크레딧입니다. 유료 충전 금액과 별도로 관리하며 현금이 아닙니다. 서버는 지급 자격과 일회성 지급을 강제합니다. 이메일을 확인하고 확인 절차를 완료하세요. 지급된 크레딧의 만료는 유료 잔액과 독립적으로 적용됩니다. 작업을 수락하기 전에 충분한 계정 자금과 키 사용 한도를 원자적으로 확보합니다. 완료 시 실제 금액을 정산하고 차액을 해제합니다. 과금할 사용량 없이 실패하면 확보 금액을 해제합니다. 공급자의 처리 결과가 불확실하면 두 번째 과금을 발생시키는 대신 대사 상태를 유지합니다. 타임아웃이나 스트림 중지는 사용량이 0이라는 근거가 아닙니다. 지원 범위에 포함되는 결제 제공자는 Payblis와 NOWPayments뿐입니다. 구성된 결제 채널은 호스팅 결제 주문을 생성하며, 자격 증명이 없으면 사용할 수 없는 상태를 반환합니다. 성공 리디렉션만으로 계정에 입금하지 않습니다. 검증된 콜백과 독립적인 대사 절차에서 통화, 금액, 제공자 참조 및 주문 식별 정보를 확인한 후 고유한 원장 항목을 기록해야 합니다. 구현된 주문 상태는 `creating`, `pending`, `confirming`, `paid`, `underpaid`, `expired` 및 `failed`입니다. 부분 결제는 전액 결제가 확인될 때까지 `underpaid` 상태를 유지합니다. 이번 릴리스에는 환불 API와 자동 환불 처리가 구현되어 있지 않습니다. 향후 환불을 구현할 때도 과거 거래를 다시 쓰는 대신 상쇄하는 원장 항목을 추가해야 합니다. [키 관리](/docs?page=keys&lang=ko) · [프로덕션 연동](/docs?page=production&lang=ko) ## 체험 자격 및 차감 순서 $0.20 크레딧을 받기 전에 계정 이메일을 확인하고 확인 절차를 완료하세요. 수령 처리는 멱등성을 보장하며 요청 빈도 제한이 적용됩니다. 크레딧은 지급 시점으로부터 정확히 7일 후 만료됩니다. 유효한 프로모션 금액은 유료 금액보다 먼저 사용합니다. 체험 요청에도 동일한 적용 가격과 키/계정 제한을 사용합니다. 사전 확보 시 자금 출처의 귀속 정보를 유지합니다. 크레딧이 만료된 후 확보 금액을 해제하면 프로모션 부분은 사용 가능한 유료 잔액이 아니라 만료된 프로모션 잔액으로 돌아갑니다. 만료로 유료 금액이 사라지는 일은 없습니다. 금융 원장 이력은 영구 보존하며, 30일 요청 로그와 독립적입니다. --- # API key 관리 키는 콘솔에서 관리합니다. `GET /v1/key`는 현재 키를 읽기 전용으로 조회하는 API입니다. 각 키에는 변경할 수 없는 ID, 표시 이름, 마스킹된 비밀 키, 상태, 생성 시각, 선택적 만료 시각, 사용 한도 및 사용량 귀속 정보가 있습니다. 계정 잔액과 키 사용 한도가 모두 요청 비용을 충당해야 합니다. 미정산 사전 확보 금액도 양쪽에 반영되므로, 병렬 호출이 오래된 합계 값을 확인하여 초과 지출하는 것을 방지합니다. 키 한도가 무제한이어도 계정 자금은 필요합니다. 한도를 이미 점유한 금액 아래로 낮추면 새 작업은 차단되지만, 기존 지출을 삭제하거나 수락된 작업을 취소하지는 않습니다. 일시 중지는 새 호출을 거부하며 되돌릴 수 있습니다. 폐기는 새 호출을 영구적으로 거부합니다. 만료된 키는 소유자에게 잔액이 있어도 API를 호출할 수 없습니다. 기존 작업은 계속 실행되고 제출에 사용한 키 ID를 유지합니다. 교체 키는 별도의 사용 이력을 시작합니다. Playground는 로그인한 세션을 사용하며, 출처 `playground`로 별도 표시합니다. 사용자의 영구 키를 노출하거나 요구하지 않습니다. 계정 차트는 Playground와 외부 API 사용량을 합산합니다. 키로 필터링한 차트에는 해당 키만 표시하며, 보관되거나 폐기된 키도 과거 이력 필터에서 계속 선택할 수 있습니다. 서버는 평문 비밀 키를 한 번만 반환하며, 이후 조회에서는 복구할 수 없습니다. [인증](/docs?page=auth&lang=ko) · [과금](/docs?page=billing&lang=ko) ## 제한 필드 각 키는 하나의 사용 상한과 하나의 기간을 선택합니다. 기간은 lifetime, daily, weekly 또는 monthly입니다. 기간은 수정할 수 있으며, 이력을 삭제하지 않고 현재 기간에 해당하는 정산 지출을 다시 계산합니다. 일간, 주간 및 월간 기간은 각각 UTC 자정, 월요일 00:00 UTC, 매월 1일 00:00 UTC에 초기화됩니다. 기간 경계 이전에 수락된 요청의 확보 금액을 포함한 모든 미정산 사전 확보 금액은 현재 한도에 반영되므로, 진행 중인 작업이 새로 초기화된 한도를 우회하지 못합니다. 정산된 지출은 요청 수락 시각이 속한 기간에 귀속되며, lifetime에는 전체 이력을 포함합니다. 모델 허용 목록, RPM, IPv4/IPv6 주소 및 CIDR 허용 목록은 각각 독립적인 제한을 추가합니다. 비어 있는 모델/IP 허용 목록은 제한 없음을 의미하며 UI에서 이를 명시합니다. 두 주소 체계를 모두 지원합니다. 계정에 대한 플랫폼 상한도 계속 적용됩니다. 키의 남은 한도는 계정 잔액이 아닙니다. 프로덕션에서 제한을 정확히 적용하려면 신뢰할 프록시 설정이 올바르게 구성되어야 합니다. GET /v1/key는 id, name, mask, enabled, revoked, limit, period, expiresAt, models, rpm, ips, spent, reserved 및 nextReset을 반환합니다. 키 금액 필드는 숫자형 USD를 사용합니다. limit=null은 키 사용 상한이 없음을 의미하며, limit=0은 양수 금액의 지출을 허용하지 않습니다. 계정 잔액 제한은 계속 적용됩니다. 비밀 키는 반환하지 않습니다. --- # 오류 및 제한 ```json {"error":{"code":"KEY_BUDGET_EXCEEDED","message":"This key has insufficient spending allowance.","request_id":"req_demo_error"}} ``` | HTTP | 코드 | 복구 방법 | |---|---|---| | 422 | `INVALID_INPUT` | 입력을 수정한 후 다시 제출하세요. | | 401 | `INVALID_API_KEY`, `API_KEY_INACTIVE` | 유효한 자격 증명을 사용하세요. | | 403 | `MODEL_NOT_ALLOWED`, `IP_NOT_ALLOWED` | 콘솔에서 키 제한을 확인하세요. | | 402 | `INSUFFICIENT_BALANCE`, `KEY_BUDGET_EXCEEDED` | 충전하거나 승인된 한도를 변경하세요. | | 404 | `MODEL_NOT_FOUND`, `REQUEST_NOT_FOUND` | ID와 소유권을 확인하세요. | | 409 | `IDEMPOTENCY_CONFLICT` | 최초 페이로드를 사용하거나 새로운 논리 요청을 만드세요. | | 409 | 진행 중인 요청 레코드 또는 `RESPONSE_CACHE_EXPIRED` | 기다리거나 대사하세요. 중복 생성에 다시 비용을 지불하지 마세요. | | 422 | `UNSUPPORTED_PARAMETER` | 모델 또는 입력 선택을 수정하세요. | | 429 | `RATE_LIMITED` | Retry-After를 준수하고 트래픽을 줄이세요. | | 502/503 | `MODEL_UNAVAILABLE` | 제출을 재시도하기 전에 요청 상태를 확인하세요. | 메시지에서는 민감한 정보를 제거합니다. 공급자 잔액, 가맹점 비밀 정보 및 원본 상위 서비스 스택 추적은 표시하지 않습니다. `X-Request-ID`를 문제 해결에 사용할 수 있습니다. 원인을 알 수 없는 공급자 오류를 콘텐츠 정책 위반으로 잘못 분류해서는 안 됩니다. SSE 헤더를 전송한 후의 오류는 두 번째 HTTP 응답이 아니라 SSE 오류 이벤트로 전달한 뒤 연결을 종료합니다. [멱등성](/docs?page=tasks&lang=ko) · [프로덕션 연동](/docs?page=production&lang=ko) --- # 프로덕션 연동 이 참조 문서는 서비스 구현에 대응하며, 배포와 통합 검증은 별도 단계입니다. 실제 트래픽을 시작하기 전에 별도의 테스트 환경에서 각 공급자 경로, 매개변수 스키마, 가격 계산, 스트리밍 사용량, 결제 서명 및 장애 복구 경로를 검증하세요. - 서버 측 키, TLS, 명시적인 타임아웃, 제한된 폴링 및 요청 ID를 사용하세요. - 제출 전에 멱등성 레코드를 영구 저장하세요. 전송 실패로 결과가 불확실할 때 과금되는 상위 POST 요청을 무조건 재시도해서는 안 됩니다. - 지갑 원장, 사전 확보 금액, 키 예산 및 결제 정산은 MySQL 트랜잭션으로 일관성을 유지하세요. DynamoDB는 요청 이벤트/조회용 프로젝션과 30일 요청 로그 보존에 사용하며, 지갑의 권위 있는 상태를 별도로 관리하는 저장소로 사용하지 마세요. - 사전 확보와 처리 권한 획득 기록을 저장한 후 직접 제출하세요. 상위 서비스의 ID를 받은 후에만 비동기 작업을 반환합니다. 작업 GET은 폴링과 정산을 수행합니다. 주기적 대사 프로세스는 조회되지 않은 작업을 확인하고 실패한 로그 쓰기를 재시도합니다. - 기본 로그는 메타데이터만 포함하며 30일 후 만료됩니다. 프롬프트, 채팅 내용, 입력 파일, 생성 파일, Authorization 헤더 또는 결제 비밀 정보를 이 로그에 저장하지 마세요. - 원장과 결제 기록에는 별도의 보존 정책을 적용합니다. 요청 메타데이터를 삭제해도 금융 이력을 삭제해서는 안 됩니다. 프로덕션 보존 요구 사항은 출시 전에 확정해야 합니다. - 대사 적체, 해제되지 않는 확보 금액, 중복 정산 시도, 작업 지연, 로그 재시도 적체, 공급자 오류 및 결제 불일치를 모니터링하고 알림을 설정하세요. 출시 전에 자격 증명, DNS/TLS, 워크로드 권한을 설정하고 실제 연동을 검증하세요. [작업](/docs?page=tasks&lang=ko) · [과금](/docs?page=billing&lang=ko) MySQL은 작업 조정, 금융 트랜잭션 및 로그 재시도 대기 레코드의 기준 저장소입니다. 계정/키/시간별 프로젝션, 날짜 버킷 및 LOG_SHARDS를 사용하여 메타데이터를 DynamoDB에 직접 기록하고, 조회 시 만료된 레코드를 제외하세요. 경량 대사 프로세스는 조회되지 않은 작업을 폴링하고 로그 쓰기를 재시도합니다. 웹, 게이트웨이 및 대사 프로세스는 별도로 배포합니다. 성공 응답은 생성 결과의 원본 URL을 변경 없이 반환합니다. 가용성, 만료 및 교차 출처 제한은 원본 호스트가 결정합니다. 만료 전에 결과를 저장하세요. 완료된 작업을 다시 조회해도 URL이 갱신되지 않습니다. 복구용 메타데이터와 URL은 보관할 수 있지만 생성 파일은 보관하지 않습니다. --- # 참조 URL 및 임시 입력 모델 메타데이터가 `image`를 지원하면 이미지 또는 동영상 요청의 최상위 필드에 HTTPS 참조 이미지 URL 하나를 전달하세요. 서비스는 브라우저의 `blob:` URL에 접근할 수 없습니다. 여러 참조 이미지는 현재 정규화된 요청에 포함되지 않습니다. 참조 이미지를 지원하는 모델에서는 `image`에 공개 HTTPS URL을 전달합니다. 공개 추론 API는 URL만 받으며 파일 바이트는 받지 않습니다. Playground에서는 로그인하고 검증된 사용자만 임시 이미지나 동영상을 비공개 R2에 업로드할 수 있습니다. 생성 결과는 다운로드하거나 저장하거나 재호스팅하지 않습니다. 임시 입력은 PNG/JPEG/WebP 최대 10 MiB, MP4/WebM 최대 50 MiB를 지원하며 계정당 분당 20회 업로드를 시도할 수 있습니다. 읽기 링크는 최대 1시간 유효하고 갱신되지 않습니다. 객체는 1일 만료 규칙으로 비동기 삭제되며 정확히 24시간 후의 물리적 삭제는 보장되지 않습니다. 이미지 URL은 지원되는 참조 이미지 필드에 사용할 수 있습니다. 동영상 URL은 동영상 입력을 지원하는 모델에서만 사용할 수 있고 참조 이미지로 사용할 수 없습니다. 외부 입력 URL은 처리하는 동안 접근 가능해야 합니다. ```json {"model":"seedream-4.5","prompt":"A small blue house","image":"https://example.com/reference.png","n":1,"size":"2048x2048"} ``` 성공 응답은 생성 결과의 원본 URL을 변경 없이 반환합니다. 가용성, 만료 및 교차 출처 제한은 원본 호스트가 결정합니다. 만료 전에 결과를 저장하세요. 완료된 작업을 다시 조회해도 URL이 갱신되지 않습니다. 복구용 메타데이터와 URL은 보관할 수 있지만 생성 파일은 보관하지 않습니다. --- # Python 연동 가이드 예제는 서버에서 실행하세요. 플랫폼 전용 SDK 대신 각 언어의 HTTP 클라이언트를 사용합니다. 실행 명령을 실행하기 전에는 요청이 전송되지 않습니다. 모델은 먼저 설정하고 활성화해야 합니다. ## 환경 준비 실행 환경: **Python 3.11+**. [전체 소스 다운로드](/examples/python.py). 배포 주소와 서버용 API Key를 환경 변수로 설정하세요. URL 끝에 `/v1`을 한 번만 포함하세요. `https://xxxrouter.com`은 공식 서비스 도메인입니다. 로컬 테스트에서는 자신의 배포 환경 origin으로 바꾸세요. 현재 아직 배포되지 않았으며 연동 검증도 완료되지 않았습니다. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 호출 방식 선택 `chat`은 SSE 증분 내용과 최종 사용량을 읽습니다. `image`는 동기 이미지 결과를 받고, `async-image`와 `video`는 HTTP 202의 작업을 조회합니다. 폴링은 약 5분 후 중지됩니다(진행 중인 조회 시간은 별도). 시간 초과 후에도 작업 ID를 보관하고 나중에 조회할 수 있습니다. ```sh python3 python.py chat # python3 python.py image # python3 python.py async-image # python3 python.py video ``` ## 멱등성과 오류 복구 다시 실행하면 기본적으로 새 멱등성 키가 생성되며 새 요금이 발생할 수 있습니다. 애플리케이션에서는 전송 전에 키와 전체 요청 본문을 영구 저장하세요. 모델, 본문, API Key, 호출 출처가 모두 같은 논리적 요청을 재개할 때만 `XXXROUTER_REQUEST_ID`에 원래 값을 지정하세요. 통신 오류 후 새 키로 다시 전송하지 마세요. 각 클라이언트는 생성 POST를 한 번만 전송하며 자동 재시도하지 않습니다. 응답 헤더의 `X-Request-ID`를 보관하세요. HTTP 오류는 진단할 수 있도록 응답 본문을 유지합니다. `status=unknown`인 작업은 자동 폴링을 중지하고 이후 조회 또는 정산 확인을 기다립니다. 성공이나 무료 호출로 간주하지 않습니다. 스트림 오류나 `[DONE]` 누락은 중단을 나타냅니다. 사용량 누락이 요금 0을 의미하지는 않습니다. 연결을 끊어도 수락된 작업은 취소되지 않습니다. API Key나 서명된 미디어 URL을 로그에 남기지 마세요. ## 참조 이미지와 결과 파일 참조 이미지를 지원하는 모델에서는 `image`에 공개 HTTPS URL을 전달합니다. 공개 추론 API는 URL만 받으며 파일 바이트는 받지 않습니다. Playground에서는 로그인하고 검증된 사용자만 임시 이미지나 동영상을 비공개 R2에 업로드할 수 있습니다. 생성 결과는 다운로드하거나 저장하거나 재호스팅하지 않습니다. 성공 응답은 생성 결과의 원본 URL을 변경 없이 반환합니다. 가용성, 만료 및 교차 출처 제한은 원본 호스트가 결정합니다. 만료 전에 결과를 저장하세요. 완료된 작업을 다시 조회해도 URL이 갱신되지 않습니다. 복구용 메타데이터와 URL은 보관할 수 있지만 생성 파일은 보관하지 않습니다. ## 전체 예제 이 코드는 연동 예제이며 실제 모델 또는 결제 채널의 활성화를 보장하지 않습니다. 브라우저 번들에 포함하지 마세요. ```python """Python 3.11+, standard library only. No automatic generation retries.""" import json import os import sys import time import urllib.error import urllib.request import uuid BASE = os.environ['XXXROUTER_BASE_URL'].rstrip('/') # Include /v1 exactly once. KEY = os.environ['XXXROUTER_API_KEY'] MODE = sys.argv[1] if len(sys.argv) > 1 else 'chat' CASES = { 'chat': ('chat/completions', {'model': 'deepseek-v4-flash', 'messages': [{'role': 'user', 'content': 'Explain HTTP in one sentence.'}], 'max_tokens': 256, 'stream': True, 'stream_options': {'include_usage': True}}), 'image': ('images/generations', {'model': 'seedream-4.5', 'prompt': 'A ceramic cup in natural light', 'size': '2048x2048', 'n': 1}), 'async-image': ('images/generations', {'model': 'nano-banana-pro', 'prompt': 'A ceramic cup in natural light', 'size': '1024x1024', 'n': 1}), 'video': ('videos/generations', {'model': 'kling-3.0', 'prompt': 'A slow camera move across a mountain landscape', 'duration': 5, 'aspect_ratio': '16:9'}), } class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None opener = urllib.request.build_opener(NoRedirect) def request(path, payload=None, idem=None): headers = {'Authorization': 'Bearer ' + KEY} if idem: headers['Idempotency-Key'] = idem data = None if payload is not None: headers['Content-Type'] = 'application/json' data = json.dumps(payload).encode() req = urllib.request.Request(BASE + '/' + path, data=data, headers=headers) try: return opener.open(req, timeout=300 if payload is not None else 30) except urllib.error.HTTPError as error: # Read the structured failure: an uncertain request may already have a hold. print('HTTP', error.code, 'request_id=', error.headers.get('X-Request-ID'), file=sys.stderr) print(error.read().decode(), file=sys.stderr) raise def stream(response): event = [] def consume(): data = '\n'.join(event) if data == '[DONE]': return True if data: chunk = json.loads(data) if chunk.get('error'): raise RuntimeError(chunk['error']) for choice in chunk.get('choices', []): print(choice.get('delta', {}).get('content') or '', end='', flush=True) if chunk.get('usage'): print('\nusage:', chunk['usage']) return False for raw in response: line = raw.decode('utf-8').rstrip('\r\n') if line == '': if consume(): return event.clear() elif line.startswith('data:'): event.append(line[5:].lstrip(' ')) if event and consume(): return raise RuntimeError('Stream interrupted. Keep request ID and idempotency key; do not create another submission.') def finish(result): deadline = time.monotonic() + 300 while result.get('task_id') and result.get('status') not in ('succeeded', 'failed', 'unknown'): if time.monotonic() >= deadline: print(result) raise TimeoutError('Keep task_id and query later; do not resubmit.') time.sleep(2) with request('tasks/' + result['task_id']) as response: result = json.load(response) print(json.dumps(result, indent=2)) if result.get('status') == 'unknown': raise RuntimeError('Outcome unconfirmed. Retain IDs and wait for reconciliation; do not resubmit.') if result.get('status') == 'failed': raise RuntimeError('Generation failed. Inspect error and billing before a new logical request.') if __name__ == '__main__': path, payload = CASES[MODE] idem = os.environ.get('XXXROUTER_REQUEST_ID') or str(uuid.uuid4()) print('idempotency_key=', idem, file=sys.stderr) # Persist the key with this exact payload before submitting in a real application. with request(path, payload, idem) as response: print('request_id=', response.headers.get('X-Request-ID'), file=sys.stderr) if 'text/event-stream' in response.headers.get('Content-Type', ''): stream(response) else: finish(json.load(response)) ``` ## 관련 문서 [chat](/docs?page=chat&lang=ko) · [images](/docs?page=images&lang=ko) · [videos](/docs?page=videos&lang=ko) · [tasks](/docs?page=tasks&lang=ko) · [uploads](/docs?page=uploads&lang=ko) · [errors](/docs?page=errors&lang=ko) [언어 공식 문서](https://docs.python.org/3/library/urllib.request.html) · 2026-10-26 --- # TypeScript / Node.js 연동 가이드 예제는 서버에서 실행하세요. 플랫폼 전용 SDK 대신 각 언어의 HTTP 클라이언트를 사용합니다. 실행 명령을 실행하기 전에는 요청이 전송되지 않습니다. 모델은 먼저 설정하고 활성화해야 합니다. ## 환경 준비 실행 환경: **Node.js 22.18+**. [전체 소스 다운로드](/examples/typescript.ts). 배포 주소와 서버용 API Key를 환경 변수로 설정하세요. URL 끝에 `/v1`을 한 번만 포함하세요. `https://xxxrouter.com`은 공식 서비스 도메인입니다. 로컬 테스트에서는 자신의 배포 환경 origin으로 바꾸세요. 현재 아직 배포되지 않았으며 연동 검증도 완료되지 않았습니다. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 호출 방식 선택 `chat`은 SSE 증분 내용과 최종 사용량을 읽습니다. `image`는 동기 이미지 결과를 받고, `async-image`와 `video`는 HTTP 202의 작업을 조회합니다. 폴링은 약 5분 후 중지됩니다(진행 중인 조회 시간은 별도). 시간 초과 후에도 작업 ID를 보관하고 나중에 조회할 수 있습니다. ```sh node --experimental-strip-types typescript.ts chat # node --experimental-strip-types typescript.ts image # node --experimental-strip-types typescript.ts async-image # node --experimental-strip-types typescript.ts video ``` ## 멱등성과 오류 복구 다시 실행하면 기본적으로 새 멱등성 키가 생성되며 새 요금이 발생할 수 있습니다. 애플리케이션에서는 전송 전에 키와 전체 요청 본문을 영구 저장하세요. 모델, 본문, API Key, 호출 출처가 모두 같은 논리적 요청을 재개할 때만 `XXXROUTER_REQUEST_ID`에 원래 값을 지정하세요. 통신 오류 후 새 키로 다시 전송하지 마세요. 각 클라이언트는 생성 POST를 한 번만 전송하며 자동 재시도하지 않습니다. 응답 헤더의 `X-Request-ID`를 보관하세요. HTTP 오류는 진단할 수 있도록 응답 본문을 유지합니다. `status=unknown`인 작업은 자동 폴링을 중지하고 이후 조회 또는 정산 확인을 기다립니다. 성공이나 무료 호출로 간주하지 않습니다. 스트림 오류나 `[DONE]` 누락은 중단을 나타냅니다. 사용량 누락이 요금 0을 의미하지는 않습니다. 연결을 끊어도 수락된 작업은 취소되지 않습니다. API Key나 서명된 미디어 URL을 로그에 남기지 마세요. ## 참조 이미지와 결과 파일 참조 이미지를 지원하는 모델에서는 `image`에 공개 HTTPS URL을 전달합니다. 공개 추론 API는 URL만 받으며 파일 바이트는 받지 않습니다. Playground에서는 로그인하고 검증된 사용자만 임시 이미지나 동영상을 비공개 R2에 업로드할 수 있습니다. 생성 결과는 다운로드하거나 저장하거나 재호스팅하지 않습니다. 성공 응답은 생성 결과의 원본 URL을 변경 없이 반환합니다. 가용성, 만료 및 교차 출처 제한은 원본 호스트가 결정합니다. 만료 전에 결과를 저장하세요. 완료된 작업을 다시 조회해도 URL이 갱신되지 않습니다. 복구용 메타데이터와 URL은 보관할 수 있지만 생성 파일은 보관하지 않습니다. ## 전체 예제 이 코드는 연동 예제이며 실제 모델 또는 결제 채널의 활성화를 보장하지 않습니다. 브라우저 번들에 포함하지 마세요. ```typescript // Node.js 22.18+; run: node --experimental-strip-types typescript.ts chat // Uses native fetch, including explicit HTTP and SSE error handling. import { randomUUID } from 'node:crypto'; type Result = { status?: string; task_id?: string; error?: unknown; [key: string]: any }; const base = process.env.XXXROUTER_BASE_URL?.replace(/\/$/, ''); // Include /v1. const key = process.env.XXXROUTER_API_KEY; if (!base || !key) throw new Error('Set XXXROUTER_BASE_URL and XXXROUTER_API_KEY'); const cases: Record]> = { chat: ['chat/completions', { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'Explain HTTP in one sentence.' }], max_tokens: 256, stream: true, stream_options: { include_usage: true } }], image: ['images/generations', { model: 'seedream-4.5', prompt: 'A ceramic cup in natural light', size: '2048x2048', n: 1 }], 'async-image': ['images/generations', { model: 'nano-banana-pro', prompt: 'A ceramic cup in natural light', size: '1024x1024', n: 1 }], video: ['videos/generations', { model: 'kling-3.0', prompt: 'A slow camera move across a mountain landscape', duration: 5, aspect_ratio: '16:9' }], }; async function request(path: string, payload?: unknown, idem?: string): Promise { const headers: Record = { Authorization: `Bearer ${key}` }; if (idem) headers['Idempotency-Key'] = idem; if (payload) headers['Content-Type'] = 'application/json'; const response = await fetch(`${base}/${path}`, { method: payload ? 'POST' : 'GET', headers, redirect: 'error', body: payload ? JSON.stringify(payload) : undefined, signal: AbortSignal.timeout(payload ? 300_000 : 30_000), }); console.error('request_id=', response.headers.get('X-Request-ID')); if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); return response; } async function stream(response: Response): Promise { if (!response.body) throw new Error('Empty response body'); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ''; let data: string[] = []; const consume = (): boolean => { const value = data.join('\n'); data = []; if (value === '[DONE]') return true; if (!value) return false; const chunk = JSON.parse(value); if (chunk.error) throw new Error(JSON.stringify(chunk.error)); for (const choice of chunk.choices ?? []) process.stdout.write(choice.delta?.content ?? ''); if (chunk.usage) console.log('\nusage:', chunk.usage); return false; }; try { while (true) { const { value, done } = await reader.read(); buffer += value ?? ''; let end: number; while ((end = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, end).replace(/\r$/, ''); buffer = buffer.slice(end + 1); if (!line && consume()) return; if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, '')); } if (done) break; } if (buffer.startsWith('data:')) data.push(buffer.slice(5).trimStart()); if (consume()) return; throw new Error('Stream interrupted. Keep request ID and idempotency key; do not resubmit.'); } finally { await reader.cancel(); reader.releaseLock(); } } async function finish(result: Result): Promise { const deadline = Date.now() + 300_000; while (result.task_id && !['succeeded', 'failed', 'unknown'].includes(result.status ?? '')) { if (Date.now() >= deadline) throw new Error(`Query task ${result.task_id} later; do not resubmit`); await new Promise(resolve => setTimeout(resolve, 2000)); result = await (await request(`tasks/${encodeURIComponent(result.task_id)}`)).json(); } console.log(result); if (result.status === 'unknown') throw new Error('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (result.status === 'failed') throw new Error('Generation failed; inspect error and billing.'); } const [path, payload] = cases[process.argv[2] ?? 'chat']; const idem = process.env.XXXROUTER_REQUEST_ID || randomUUID(); console.error('idempotency_key=', idem); // Persist alongside the exact payload before submitting. const response = await request(path, payload, idem); if (response.headers.get('content-type')?.includes('text/event-stream')) await stream(response); else await finish(await response.json()); ``` ## 관련 문서 [chat](/docs?page=chat&lang=ko) · [images](/docs?page=images&lang=ko) · [videos](/docs?page=videos&lang=ko) · [tasks](/docs?page=tasks&lang=ko) · [uploads](/docs?page=uploads&lang=ko) · [errors](/docs?page=errors&lang=ko) [언어 공식 문서](https://nodejs.org/api/globals.html#fetch) · 2026-10-26 --- # Go 연동 가이드 예제는 서버에서 실행하세요. 플랫폼 전용 SDK 대신 각 언어의 HTTP 클라이언트를 사용합니다. 실행 명령을 실행하기 전에는 요청이 전송되지 않습니다. 모델은 먼저 설정하고 활성화해야 합니다. ## 환경 준비 실행 환경: **Go 1.22+**. [전체 소스 다운로드](/examples/go.go). 배포 주소와 서버용 API Key를 환경 변수로 설정하세요. URL 끝에 `/v1`을 한 번만 포함하세요. `https://xxxrouter.com`은 공식 서비스 도메인입니다. 로컬 테스트에서는 자신의 배포 환경 origin으로 바꾸세요. 현재 아직 배포되지 않았으며 연동 검증도 완료되지 않았습니다. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 호출 방식 선택 `chat`은 SSE 증분 내용과 최종 사용량을 읽습니다. `image`는 동기 이미지 결과를 받고, `async-image`와 `video`는 HTTP 202의 작업을 조회합니다. 폴링은 약 5분 후 중지됩니다(진행 중인 조회 시간은 별도). 시간 초과 후에도 작업 ID를 보관하고 나중에 조회할 수 있습니다. ```sh go run go.go chat # go run go.go image # go run go.go async-image # go run go.go video ``` ## 멱등성과 오류 복구 다시 실행하면 기본적으로 새 멱등성 키가 생성되며 새 요금이 발생할 수 있습니다. 애플리케이션에서는 전송 전에 키와 전체 요청 본문을 영구 저장하세요. 모델, 본문, API Key, 호출 출처가 모두 같은 논리적 요청을 재개할 때만 `XXXROUTER_REQUEST_ID`에 원래 값을 지정하세요. 통신 오류 후 새 키로 다시 전송하지 마세요. 각 클라이언트는 생성 POST를 한 번만 전송하며 자동 재시도하지 않습니다. 응답 헤더의 `X-Request-ID`를 보관하세요. HTTP 오류는 진단할 수 있도록 응답 본문을 유지합니다. `status=unknown`인 작업은 자동 폴링을 중지하고 이후 조회 또는 정산 확인을 기다립니다. 성공이나 무료 호출로 간주하지 않습니다. 스트림 오류나 `[DONE]` 누락은 중단을 나타냅니다. 사용량 누락이 요금 0을 의미하지는 않습니다. 연결을 끊어도 수락된 작업은 취소되지 않습니다. API Key나 서명된 미디어 URL을 로그에 남기지 마세요. ## 참조 이미지와 결과 파일 참조 이미지를 지원하는 모델에서는 `image`에 공개 HTTPS URL을 전달합니다. 공개 추론 API는 URL만 받으며 파일 바이트는 받지 않습니다. Playground에서는 로그인하고 검증된 사용자만 임시 이미지나 동영상을 비공개 R2에 업로드할 수 있습니다. 생성 결과는 다운로드하거나 저장하거나 재호스팅하지 않습니다. 성공 응답은 생성 결과의 원본 URL을 변경 없이 반환합니다. 가용성, 만료 및 교차 출처 제한은 원본 호스트가 결정합니다. 만료 전에 결과를 저장하세요. 완료된 작업을 다시 조회해도 URL이 갱신되지 않습니다. 복구용 메타데이터와 URL은 보관할 수 있지만 생성 파일은 보관하지 않습니다. ## 전체 예제 이 코드는 연동 예제이며 실제 모델 또는 결제 채널의 활성화를 보장하지 않습니다. 브라우저 번들에 포함하지 마세요. ```go // Go 1.22+, standard library only. Run: go run go.go chat package main import ( "bufio" "bytes" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) type Object = map[string]any var base = strings.TrimRight(os.Getenv("XXXROUTER_BASE_URL"), "/") // Include /v1. var key = os.Getenv("XXXROUTER_API_KEY") var client = &http.Client{Timeout: 300 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} func request(path string, payload Object, idem string) (*http.Response, error) { var body io.Reader method := "GET" if payload != nil { raw, err := json.Marshal(payload) if err != nil { return nil, err } body = bytes.NewReader(raw) method = "POST" } req, err := http.NewRequest(method, base+"/"+path, body) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) if payload != nil { req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", idem) } response, err := client.Do(req) if err != nil { return nil, err } fmt.Fprintln(os.Stderr, "request_id=", response.Header.Get("X-Request-ID")) if response.StatusCode < 200 || response.StatusCode >= 300 { defer response.Body.Close() raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20)) return nil, fmt.Errorf("HTTP %d: %s", response.StatusCode, raw) } return response, nil } func decode(response *http.Response) (Object, error) { defer response.Body.Close() var result Object err := json.NewDecoder(response.Body).Decode(&result) return result, err } func stream(response *http.Response) error { defer response.Body.Close() scanner := bufio.NewScanner(response.Body) scanner.Buffer(make([]byte, 4096), 2<<20) var parts []string consume := func() (bool, error) { data := strings.Join(parts, "\n") parts = nil if data == "[DONE]" { return true, nil } if data == "" { return false, nil } var chunk struct { Error json.RawMessage `json:"error"` Usage json.RawMessage `json:"usage"` Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` } if err := json.Unmarshal([]byte(data), &chunk); err != nil { return false, err } if len(chunk.Error) > 0 && string(chunk.Error) != "null" { return false, fmt.Errorf("stream error: %s", chunk.Error) } for _, choice := range chunk.Choices { fmt.Print(choice.Delta.Content) } if len(chunk.Usage) > 0 && string(chunk.Usage) != "null" { fmt.Printf("\nusage: %s\n", chunk.Usage) } return false, nil } for scanner.Scan() { line := scanner.Text() if line == "" { done, err := consume() if err != nil || done { return err } } if strings.HasPrefix(line, "data:") { parts = append(parts, strings.TrimPrefix(line[5:], " ")) } } if err := scanner.Err(); err != nil { return err } done, err := consume() if err != nil || done { return err } return fmt.Errorf("stream interrupted; keep request ID and idempotency key, do not resubmit") } func finish(result Object) error { deadline := time.Now().Add(5 * time.Minute) for { task, hasTask := result["task_id"].(string) status, _ := result["status"].(string) if !hasTask || status == "succeeded" || status == "failed" || status == "unknown" { break } if time.Now().After(deadline) { return fmt.Errorf("query task %s later; do not resubmit", task) } time.Sleep(2 * time.Second) response, err := request("tasks/"+task, nil, "") if err != nil { return err } result, err = decode(response) if err != nil { return err } } raw, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(raw)) if result["status"] == "unknown" { return fmt.Errorf("outcome unconfirmed; retain IDs and wait for reconciliation") } if result["status"] == "failed" { return fmt.Errorf("generation failed; inspect error and billing") } return nil } func run() error { if base == "" || key == "" { return fmt.Errorf("set XXXROUTER_BASE_URL and XXXROUTER_API_KEY") } mode := "chat" if len(os.Args) > 1 { mode = os.Args[1] } path := "images/generations" var payload Object switch mode { case "chat": path = "chat/completions" payload = Object{"model": "deepseek-v4-flash", "messages": []Object{{"role": "user", "content": "Explain HTTP in one sentence."}}, "max_tokens": 256, "stream": true, "stream_options": Object{"include_usage": true}} case "image": payload = Object{"model": "seedream-4.5", "prompt": "A ceramic cup in natural light", "size": "2048x2048", "n": 1} case "async-image": payload = Object{"model": "nano-banana-pro", "prompt": "A ceramic cup in natural light", "size": "1024x1024", "n": 1} case "video": path = "videos/generations" payload = Object{"model": "kling-3.0", "prompt": "A slow camera move across a mountain landscape", "duration": 5, "aspect_ratio": "16:9"} default: return fmt.Errorf("choose chat, image, async-image or video") } idem := os.Getenv("XXXROUTER_REQUEST_ID") if idem == "" { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return err } idem = hex.EncodeToString(b) } fmt.Fprintln(os.Stderr, "idempotency_key=", idem) // Persist with the exact payload before submission. response, err := request(path, payload, idem) if err != nil { return err } if strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { return stream(response) } result, err := decode(response) if err != nil { return err } return finish(result) } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } ``` ## 관련 문서 [chat](/docs?page=chat&lang=ko) · [images](/docs?page=images&lang=ko) · [videos](/docs?page=videos&lang=ko) · [tasks](/docs?page=tasks&lang=ko) · [uploads](/docs?page=uploads&lang=ko) · [errors](/docs?page=errors&lang=ko) [언어 공식 문서](https://pkg.go.dev/net/http) · 2026-10-26 --- # PHP 연동 가이드 예제는 서버에서 실행하세요. 플랫폼 전용 SDK 대신 각 언어의 HTTP 클라이언트를 사용합니다. 실행 명령을 실행하기 전에는 요청이 전송되지 않습니다. 모델은 먼저 설정하고 활성화해야 합니다. ## 환경 준비 실행 환경: **PHP 8.1+ + ext-curl**. [전체 소스 다운로드](/examples/php.php). 배포 주소와 서버용 API Key를 환경 변수로 설정하세요. URL 끝에 `/v1`을 한 번만 포함하세요. `https://xxxrouter.com`은 공식 서비스 도메인입니다. 로컬 테스트에서는 자신의 배포 환경 origin으로 바꾸세요. 현재 아직 배포되지 않았으며 연동 검증도 완료되지 않았습니다. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## 호출 방식 선택 `chat`은 SSE 증분 내용과 최종 사용량을 읽습니다. `image`는 동기 이미지 결과를 받고, `async-image`와 `video`는 HTTP 202의 작업을 조회합니다. 폴링은 약 5분 후 중지됩니다(진행 중인 조회 시간은 별도). 시간 초과 후에도 작업 ID를 보관하고 나중에 조회할 수 있습니다. ```sh php php.php chat # php php.php image # php php.php async-image # php php.php video ``` ## 멱등성과 오류 복구 다시 실행하면 기본적으로 새 멱등성 키가 생성되며 새 요금이 발생할 수 있습니다. 애플리케이션에서는 전송 전에 키와 전체 요청 본문을 영구 저장하세요. 모델, 본문, API Key, 호출 출처가 모두 같은 논리적 요청을 재개할 때만 `XXXROUTER_REQUEST_ID`에 원래 값을 지정하세요. 통신 오류 후 새 키로 다시 전송하지 마세요. 각 클라이언트는 생성 POST를 한 번만 전송하며 자동 재시도하지 않습니다. 응답 헤더의 `X-Request-ID`를 보관하세요. HTTP 오류는 진단할 수 있도록 응답 본문을 유지합니다. `status=unknown`인 작업은 자동 폴링을 중지하고 이후 조회 또는 정산 확인을 기다립니다. 성공이나 무료 호출로 간주하지 않습니다. 스트림 오류나 `[DONE]` 누락은 중단을 나타냅니다. 사용량 누락이 요금 0을 의미하지는 않습니다. 연결을 끊어도 수락된 작업은 취소되지 않습니다. API Key나 서명된 미디어 URL을 로그에 남기지 마세요. ## 참조 이미지와 결과 파일 참조 이미지를 지원하는 모델에서는 `image`에 공개 HTTPS URL을 전달합니다. 공개 추론 API는 URL만 받으며 파일 바이트는 받지 않습니다. Playground에서는 로그인하고 검증된 사용자만 임시 이미지나 동영상을 비공개 R2에 업로드할 수 있습니다. 생성 결과는 다운로드하거나 저장하거나 재호스팅하지 않습니다. 성공 응답은 생성 결과의 원본 URL을 변경 없이 반환합니다. 가용성, 만료 및 교차 출처 제한은 원본 호스트가 결정합니다. 만료 전에 결과를 저장하세요. 완료된 작업을 다시 조회해도 URL이 갱신되지 않습니다. 복구용 메타데이터와 URL은 보관할 수 있지만 생성 파일은 보관하지 않습니다. ## 전체 예제 이 코드는 연동 예제이며 실제 모델 또는 결제 채널의 활성화를 보장하지 않습니다. 브라우저 번들에 포함하지 마세요. ```php getMessage(); } }; $curl = curl_init($base . '/' . $path); $options = [ CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $payload !== null ? 300 : 30, CURLOPT_HEADERFUNCTION => function ($handle, string $line) use (&$contentType, &$requestId): int { if (stripos($line, 'Content-Type:') === 0) $contentType = trim(substr($line, 13)); if (stripos($line, 'X-Request-ID:') === 0) $requestId = trim(substr($line, 13)); return strlen($line); }, CURLOPT_WRITEFUNCTION => function ($handle, string $bytes) use (&$contentType, &$raw, &$buffer, &$data, &$done, &$streamError, $consume): int { if (!str_contains($contentType, 'text/event-stream')) { $raw .= $bytes; return strlen($bytes); } $buffer .= $bytes; while (($end = strpos($buffer, "\n")) !== false) { $line = rtrim(substr($buffer, 0, $end), "\r"); $buffer = substr($buffer, $end + 1); if ($line === '') $consume(); elseif (str_starts_with($line, 'data:')) $data[] = ltrim(substr($line, 5), ' '); } return strlen($bytes); }, ]; if ($payload !== null) { $headers[] = 'Content-Type: application/json'; $headers[] = 'Idempotency-Key: ' . $idem; $options[CURLOPT_POST] = true; $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_THROW_ON_ERROR); } $options[CURLOPT_HTTPHEADER] = $headers; curl_setopt_array($curl, $options); try { $ok = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); fwrite(STDERR, 'request_id=' . $requestId . "\n"); if ($ok === false) throw new RuntimeException('Transport error; retain IDs, do not resubmit: ' . curl_error($curl)); if ($status < 200 || $status >= 300) throw new RuntimeException('HTTP ' . $status . ': ' . $raw); if (str_contains($contentType, 'text/event-stream')) { if (str_starts_with($buffer, 'data:')) $data[] = ltrim(substr($buffer, 5), ' '); $consume(); if ($streamError !== null) throw new RuntimeException($streamError); if (!$done) throw new RuntimeException('Stream interrupted; retain request ID and idempotency key.'); return ['stream_complete' => true]; } return json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } finally { curl_close($curl); } } $cases = [ 'chat' => ['chat/completions', ['model'=>'deepseek-v4-flash','messages'=>[['role'=>'user','content'=>'Explain HTTP in one sentence.']],'max_tokens'=>256,'stream'=>true,'stream_options'=>['include_usage'=>true]]], 'image' => ['images/generations', ['model'=>'seedream-4.5','prompt'=>'A ceramic cup in natural light','size'=>'2048x2048','n'=>1]], 'async-image' => ['images/generations', ['model'=>'nano-banana-pro','prompt'=>'A ceramic cup in natural light','size'=>'1024x1024','n'=>1]], 'video' => ['videos/generations', ['model'=>'kling-3.0','prompt'=>'A slow camera move across a mountain landscape','duration'=>5,'aspect_ratio'=>'16:9']], ]; $mode = $argv[1] ?? 'chat'; if (!isset($cases[$mode])) throw new InvalidArgumentException('Choose chat, image, async-image or video'); [$path, $payload] = $cases[$mode]; $idem = getenv('XXXROUTER_REQUEST_ID') ?: bin2hex(random_bytes(16)); fwrite(STDERR, 'idempotency_key=' . $idem . "\n"); // Persist alongside exact payload before submission. $result = request($path, $payload, $idem); $deadline = microtime(true) + 300; while (isset($result['task_id']) && !in_array($result['status'], ['succeeded', 'failed', 'unknown'], true)) { if (microtime(true) >= $deadline) throw new RuntimeException('Query task ' . $result['task_id'] . ' later; do not resubmit.'); sleep(2); $result = request('tasks/' . rawurlencode($result['task_id'])); } echo json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . "\n"; if (($result['status'] ?? '') === 'unknown') throw new RuntimeException('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (($result['status'] ?? '') === 'failed') throw new RuntimeException('Generation failed; inspect error and billing.'); ``` ## 관련 문서 [chat](/docs?page=chat&lang=ko) · [images](/docs?page=images&lang=ko) · [videos](/docs?page=videos&lang=ko) · [tasks](/docs?page=tasks&lang=ko) · [uploads](/docs?page=uploads&lang=ko) · [errors](/docs?page=errors&lang=ko) [언어 공식 문서](https://www.php.net/manual/en/function.curl-exec.php) · 2026-10-26 --- # 변경 이력 ## 0.1.0 — 2026-10-26 — 구현 초안 - USD 계정 잔액과 키별 사용 제한을 추가했습니다. - en / zh-CN 개발자 참조 문서와 기계 판독용 OpenAPI 초안을 추가했습니다. - 동기 이미지의 직접 결과와 비동기 이미지/동영상 작업을 구분했습니다. - 선택적 SSE를 지원하는 OpenAI 스타일 텍스트 채팅을 정의했습니다. - 유효 기간 7일의 $0.20 체험 크레딧과 Payblis / NOWPayments 호스팅 결제를 추가했습니다. - 기본 요청 로그 정책을 메타데이터 전용, 30일 보존으로 설정했습니다. 이 참조 문서는 구현된 엔드포인트를 설명합니다. 배포, 실제 경로 검증 및 결제 채널 승인은 별도의 출시 요구 사항입니다. [빠른 시작](/docs?page=quickstart&lang=ko) --- # Démarrage rapide > Référence de l’implémentation · 2026-10-26 · Configurez un déploiement avant d’envoyer des requêtes. L’application gère l’authentification, les soldes et les requêtes côté serveur. `https://xxxrouter.com` est le domaine officiel du service ; pour les tests locaux, remplacez-le par l’origine de votre déploiement. Le service n’est pas encore déployé et la validation de l’intégration n’est pas terminée. L’accès à `/v1` depuis la même origine est pris en charge par la passerelle. Les modèles sont désactivés à l’initialisation ; les opérateurs doivent configurer les identifiants d’accès, valider les routes et fixer les prix avant de les activer. Cette référence ne prouve ni qu’un déploiement a été effectué, ni que les tests d’intégration sont terminés. 1. Connectez-vous à la console et vérifiez votre solde en USD. 2. Créez une clé API. Copiez-la lors de son affichage unique et conservez-la dans le gestionnaire de secrets de votre serveur. 3. Vérifiez le mode de réponse du modèle et le prix applicable. 4. Appelez l’API depuis votre backend. Suivez l’identifiant de requête renvoyé dans Usage & Logs. ```sh export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' curl https://xxxrouter.com/v1/models \ -H "Authorization: Bearer $XXXROUTER_API_KEY" ``` ```sh curl https://xxxrouter.com/v1/chat/completions \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-chat-request-001' \ -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Hello"}],"max_tokens":256,"stream":false}' ``` Le chat suit un contrat de type OpenAI. Les API d’images prennent en charge les résultats directs et les réponses sous forme de tâches ; elles ne remplacent pas universellement OpenAI Images sans adaptation. Le Playground de la console applique les mêmes prix et règles comptables que les appels API externes, via la session connectée, avec une attribution distincte de celle des clés API. Pour continuer : [authentification](/docs?page=auth&lang=fr), [capacités des modèles](/docs?page=models&lang=fr), [images](/docs?page=images&lang=fr), [erreurs](/docs?page=errors&lang=fr). Formats lisibles par machine : [OpenAPI](/openapi.json), [llms.txt](/llms.txt), [texte intégral](/llms-full.txt). --- # Authentification Envoyez `Authorization: Bearer ` avec les requêtes de génération, de consultation des tâches et de consultation de la clé. Les points d’accès GET du catalogue de modèles permettent de consulter les prix publics sans authentification ; une clé valide facultative permet d’obtenir les prix du compte. Ne placez jamais les clés dans les paramètres d’URL, les dépôts publics, les bundles client, les captures d’écran partagées ou le stockage du navigateur. Créez une véritable clé depuis un compte connecté et vérifié dans la console. Une clé utilisable n’est affichée en entier qu’à sa création. Les consultations suivantes montrent son préfixe, son suffixe et son identifiant immuable. Le serveur conserve une empreinte à sens unique calculée avec une clé, et non un texte en clair récupérable. La gestion des clés nécessite une session de console ; une clé d’inférence ne peut ni créer d’autres clés, ni modifier la facturation du compte. Pour renouveler une clé, créez sa remplaçante, déployez-la, vérifiez le trafic, puis révoquez l’ancienne. La suspension est réversible ; la révocation est définitive. L’expiration est évaluée en UTC à chaque nouvelle requête. Les tâches déjà acceptées conservent leur facturation et leur historique après une suspension, une expiration ou une révocation. Utilisez une autre clé autorisée du même compte ou la console pour les consulter. `GET /v1/key` renvoie les métadonnées de la clé courante, notamment les restrictions applicables et la marge de dépense restante. Le secret n’est jamais renvoyé. Les erreurs ne révèlent pas si une clé devinée appartient à un autre compte. [Gestion des clés](/docs?page=keys&lang=fr) · [Erreurs](/docs?page=errors&lang=fr) --- # Modèles et tarifs `GET /v1/models` répertorie les modèles disponibles pour votre compte. `GET /v1/models/{id}` renvoie les capacités, les paramètres, le mode de réponse, la disponibilité et le prix applicable. Les identifiants publics des modèles sont des noms propres à la plateforme, sans préfixe de routage ; par exemple, `/v1/models/seedream-4.5`. Les métadonnées de routage et d’infrastructure sont internes et ne sont pas renvoyées par l’API publique des modèles. | Identifiant de modèle d’exemple | Sortie | Mode de réponse | |---|---|---| | `seedream-4.5` | Image | `sync` | | `nano-banana-pro` | Image | `async` | | `flux-2-pro` | Image | `async` | | `kling-3.0` | Vidéo | `async` | | `deepseek-v4-flash` | Texte | Mode de catalogue `stream` ; JSON ou SSE sélectionné par `stream` | Ces identifiants publics de la plateforme ne garantissent pas que les modèles sont configurés ou validés pour une utilisation réelle. Le mode de réponse d’un modèle est indiqué explicitement dans les métadonnées du catalogue ; il ne doit jamais être déduit d’une portion d’URL ou du mot-clé `async` d’un langage de programmation. La validation des capacités du modèle a lieu avant toute réservation de fonds ou soumission en amont. Les prix sont exprimés en USD. Les tarifs des images dépendent des paramètres pris en charge, ceux des vidéos de leur durée dans cette version, et ceux des LLM distinguent les tokens d’entrée et de sortie. La version du prix applicable est enregistrée à l’acceptation de la requête ; les modifications ultérieures du catalogue ne la changent pas. Les estimations ne sont pas des montants définitivement facturés. [Protocole des images](/docs?page=images&lang=fr) · [Facturation](/docs?page=billing&lang=fr) ## Priorité des prix applicables Appliquez exactement une règle correspondante, dans cet ordre : tarif spécifique utilisateur × modèle, puis tarif spécifique groupe tarifaire × modèle, puis remise par défaut du groupe tarifaire, puis prix public. Un tarif spécifique peut être un prix fixe ou une remise dans la plage autorisée pour le modèle. Les règles ne se cumulent pas. Un prix inférieur au coût nécessite une confirmation explicite de l’opérateur ; si le coût fournisseur est inconnu, aucune marge fictive ne doit être affichée. Figez la règle sélectionnée, les unités et la version à l’acceptation de la requête. La liste `parameters` du modèle indique les champs normalisés de premier niveau : les images utilisent `size`, `n`, `image` ; les vidéos utilisent `duration`, `aspect_ratio`, `image` ; le chat utilise `temperature`, `max_tokens`, `stream`, `stream_options`. Il ne s’agit pas d’un dictionnaire de paramètres arbitraires transmis tels quels. Les exemples Seedream 4.5 utilisent `2048x2048` ; les limites de paramètres en production doivent être vérifiées pour chaque modèle. Les choix proposés à titre d’exemple dans l’interface ne garantissent pas les capacités réellement disponibles. Le JSON du modèle utilise name, modality (llm/image/video), mode/response_mode (stream/sync/async), parameters, sizes, supportsImage et enabled. pricing est un objet contenant currency, price et outputPrice nullable, avec des montants USD sous forme de chaînes à six décimales ; les champs de premier niveau price/outputPrice sont des nombres exprimés en USD. Un modèle désactivé ne peut pas être appelé. Les prix initiaux doivent être examinés par l’opérateur ; un coût inconnu ne sert jamais à afficher une marge. --- # Complétions de chat `POST /v1/chat/completions` accepte `model`, `messages`, `max_tokens`, `temperature`, `stream` et, facultativement, `stream_options: {"include_usage": true}`. Le contrat v0.1 couvre les messages texte avec les rôles `system`, `user` et `assistant`. Les appels d’outils, le chat multimodal, les embeddings et la Responses API ne font pas partie du contrat de cette version. ```json {"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Explain HTTP in one sentence."}],"max_tokens":256,"temperature":0.7,"stream":true,"stream_options":{"include_usage":true}} ``` Un résultat non diffusé en continu contient `id`, `object: "chat.completion"`, `model`, `choices` et `usage`. Le streaming renvoie `text/event-stream` ; chaque événement data contient un fragment de complétion dans `choices[].delta`. Avec `stream_options.include_usage=true`, l’événement final d’utilisation fourni par l’amont, s’il est présent, précède `data: [DONE]`. Conservez le `X-Request-ID` des en-têtes de réponse. Ne traitez pas un flux SSE comme un seul document JSON. La passerelle ne doit pas insérer de prompt système imposant une sortie JSON, changer silencieusement de modèle, ni relancer une complétion facturée pour corriger sa sortie. Une déconnexion ou un clic sur Stop n’efface pas les tokens déjà générés. La facturation finale utilise l’utilisation constatée ; si les données faisant autorité manquent, la requête reste en attente de rapprochement et son coût n’est jamais considéré implicitement comme nul. Chaque route doit être validée en intégration avant son activation. [Facturation](/docs?page=billing&lang=fr) · [Production](/docs?page=production&lang=fr) Toutes les requêtes POST de génération nécessitent un Idempotency-Key de 1 à 128 caractères ASCII imprimables. Utilisez une nouvelle valeur pour une nouvelle requête logique et ne réutilisez la même valeur que pour une répétition strictement identique. max_tokens est compris entre 1 et 4096 ; temperature entre 0 et 2. La répétition d’une requête de streaming ne redémarre ni ne rejoue un flux amont déjà facturé. --- # Génération d’images `POST /v1/images/generations` propose deux modes de réponse déclarés, déterminés par le catalogue de modèles. Consultez toujours les métadonnées du modèle et gérez la réponse documentée. Les deux modes acceptent `model`, `prompt`, une URL de référence `image` facultative au premier niveau, `size` et `n`. Ces champs sont normalisés par la plateforme ; les adaptateurs les convertissent vers le schéma validé du modèle. ```sh curl https://xxxrouter.com/v1/images/generations \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-image-request-001' \ -d '{"model":"seedream-4.5","prompt":"A ceramic cup in natural light","size":"2048x2048","n":1}' ``` ## Modèle synchrone : HTTP 200 ```json {"request_id":"req_demo_sync","model":"seedream-4.5","response_mode":"sync","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/cup.png"}],"billing":{"status":"settled","currency":"USD","amount":"0.030000"}} ``` L’image est renvoyée dans la réponse HTTP initiale. Aucun identifiant de tâche amont ni interrogation ultérieure d’une tâche n’est nécessaire. Le montant ci-dessus est un exemple, pas un devis. Une exécution synchrone lente reste synchrone ; un dépassement de délai côté client ne prouve pas un échec et n’autorise pas une nouvelle soumission. ## Modèle asynchrone : HTTP 202 Sélectionnez `nano-banana-pro` ou `flux-2-pro` pour recevoir : ```json {"request_id":"req_demo_async","task_id":"task_demo_01","model":"nano-banana-pro","response_mode":"async","status":"running"} ``` Interrogez [le point d’accès des tâches](/docs?page=tasks&lang=fr) jusqu’à un état terminal. Ne considérez pas `202` comme une génération terminée. Une route commune pour les images ne signifie pas que tous les modèles sont compatibles avec OpenAI Images. Les routes configurées envoient de véritables requêtes. L’absence d’identifiants d’accès ne produit jamais un résultat payant simulé. [Images de référence](/docs?page=uploads&lang=fr) · [Erreurs](/docs?page=errors&lang=fr) Le service actuel prend uniquement en charge n=1. image n’est autorisé que si les parameters du modèle le déclarent. Une réponse réussie renvoie l’URL originale du résultat sans modification. L’hôte original contrôle sa disponibilité, son expiration et les restrictions interorigines. Enregistrez le résultat avant expiration ; consulter une tâche terminée ne renouvelle pas le lien. Les métadonnées et URL peuvent être conservées pour la reprise, jamais les fichiers générés. --- # Génération de vidéos `POST /v1/videos/generations` renvoie HTTP 202 et une tâche. L’identifiant public du modèle est `kling-3.0` ; une utilisation réelle nécessite une route configurée et validée. ```json {"model":"kling-3.0","prompt":"A slow camera movement around a ceramic cup","duration":5,"aspect_ratio":"16:9"} ``` Envoyez un `Idempotency-Key` pour chaque soumission logique. Conservez `task_id` et interrogez `/v1/tasks/{task_id}`. Un résultat réussi contient l’URL de la vidéo, son type de média et la facturation finale. Ne promettez ni annulation ni pourcentage de progression si l’amont ne les prend pas réellement en charge. Aucun pourcentage précis n’est affiché sans donnée fiable. La durée indiquée dans un devis est une donnée de tarification, pas une garantie de durée exacte de la vidéo générée. Avant la mise en service, chaque route doit documenter la facturation de la durée produite et des combinaisons de paramètres. Aucun basculement automatique vers un autre modèle n’est supposé. [Tâches](/docs?page=tasks&lang=fr) · [Facturation](/docs?page=billing&lang=fr) Les paramètres normalisés de premier niveau sont `duration`, `aspect_ratio` et, facultativement, `image`. Le schéma actuel accepte 5/10 secondes et les formats 16:9, 9:16 ou 1:1 ; vérifiez la disponibilité du modèle avant tout appel. Ce projet de requête ne contient ni champ `resolution`, ni champ `parameters` imbriqué. Une réponse réussie renvoie l’URL originale du résultat sans modification. L’hôte original contrôle sa disponibilité, son expiration et les restrictions interorigines. Enregistrez le résultat avant expiration ; consulter une tâche terminée ne renouvelle pas le lien. Les métadonnées et URL peuvent être conservées pour la reprise, jamais les fichiers générés. --- # Tâches et idempotence `GET /v1/tasks/{id}` lit une tâche d’image ou de vidéo déjà acceptée. L’authentification et l’appartenance au compte sont vérifiées à chaque lecture. Une autre clé valide du même compte peut consulter la tâche ; l’identifiant de la clé d’origine reste l’attribution immuable de son utilisation. États : `submitting → running → succeeded | failed`. Un résultat incertain utilise `status: "unknown"` pour la requête et `billing.status: "reconciling"` pour la facturation ; il ne doit pas entraîner de nouvelle soumission automatique. L’état du résultat et celui de la facturation sont distincts : un résultat peut être disponible alors que le montant est encore en cours de rapprochement. ```json {"task_id":"task_demo_01","request_id":"req_demo_async","model":"nano-banana-pro","response_mode":"async","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/image.png","media_type":"image"}],"billing":{"status":"settled","currency":"USD","amount":"0.150000"}} ``` Interrogez la tâche avec un intervalle borné : commencez à 2 secondes et augmentez progressivement jusqu’à 10 secondes maximum. Respectez `Retry-After`. Réessayer un GET ne crée pas de nouvelle génération. La perte d’une réponse POST est différente : réutilisez la même clé d’idempotence et un corps strictement identique. La même combinaison compte + point d’accès + clé + empreinte de requête renvoie l’enregistrement d’origine ; un corps différent renvoie `409 IDEMPOTENCY_CONFLICT`. Les enregistrements d’idempotence sont conservés dans MySQL. Un changement de jour ne rend pas sûre la réutilisation d’une ancienne clé ni la nouvelle soumission d’une requête au résultat incertain. Pour les images synchrones, une répétition renvoie le résultat direct enregistré lorsqu’il est disponible. Une requête synchrone encore en cours ou dont le résultat est incertain renvoie HTTP `409` avec son enregistrement courant ; aucun second envoi à l’amont n’est effectué. Ne consultez pas `/tasks` avec un identifiant de requête synchrone comme s’il s’agissait d’un identifiant de tâche. [Erreurs](/docs?page=errors&lang=fr) · [Production](/docs?page=production&lang=fr) Une réponse réussie renvoie l’URL originale du résultat sans modification. L’hôte original contrôle sa disponibilité, son expiration et les restrictions interorigines. Enregistrez le résultat avant expiration ; consulter une tâche terminée ne renouvelle pas le lien. Les métadonnées et URL peuvent être conservées pour la reprise, jamais les fichiers générés. --- # Solde et facturation Le compte dispose d’un solde prépayé unique en USD. Les plafonds des clés limitent les dépenses ; ce ne sont pas des portefeuilles distincts. Affichez séparément les montants disponibles, réservés et définitivement débités. Conservez les montants en microdollars entiers ou en décimaux exacts, jamais en nombres binaires à virgule flottante. L’offre d’essai approuvée comprend $0.20 de crédit promotionnel, valable 7 jours. Ce crédit est distinct des fonds payés et ne constitue pas des espèces. Le serveur contrôle l’éligibilité et l’attribution unique. Vérifiez votre adresse e-mail et terminez l’épreuve de vérification ; l’expiration du crédit promotionnel est appliquée indépendamment des fonds payés. Avant d’accepter une requête, réservez atomiquement les fonds du compte et la marge de dépense de la clé nécessaires. À la fin du traitement, débitez le montant réel et libérez la différence. Un échec sans utilisation facturable libère la réservation. Un résultat amont inconnu reste en cours de rapprochement au lieu de déclencher une seconde dépense. Un dépassement de délai ou l’arrêt d’un flux ne prouve pas une utilisation nulle. Payblis et NOWPayments sont les seuls services de paiement pris en charge. Les canaux configurés créent des commandes avec une page de paiement hébergée ; l’absence d’identifiants d’accès renvoie un état indisponible. Une redirection de succès ne crédite jamais un compte. Les callbacks vérifiés et le rapprochement indépendant doivent contrôler la devise, le montant, la référence du canal et l’identité de la commande avant l’inscription d’une écriture unique au grand livre. Les états de commande implémentés sont `creating`, `pending`, `confirming`, `paid`, `underpaid`, `expired` et `failed`. Les paiements partiels restent `underpaid` jusqu’à confirmation du paiement intégral. Cette version ne comprend ni API de remboursement ni traitement automatisé des remboursements. Tout remboursement futur devra ajouter des écritures compensatoires au grand livre, sans réécrire les transactions historiques. [Gestion des clés](/docs?page=keys&lang=fr) · [Production](/docs?page=production&lang=fr) ## Éligibilité à l’essai et ordre d’utilisation des fonds Vérifiez l’adresse e-mail du compte et terminez l’épreuve de vérification avant de demander le crédit de $0.20. La demande est idempotente et soumise à une limitation de fréquence. Le crédit expire exactement 7 jours après son attribution ; les fonds promotionnels encore valides sont utilisés avant les fonds payés. Les requêtes d’essai utilisent le même prix applicable et les mêmes limites de clé et de compte. Les réservations conservent l’attribution de la source de financement. Si une réservation est libérée après l’expiration du crédit, sa part promotionnelle revient au solde promotionnel expiré, et non au solde payé disponible. L’expiration ne supprime jamais les fonds payés. L’historique du grand livre financier est permanent et indépendant des journaux de requêtes conservés 30 jours. --- # Gestion des clés API Gérez les clés dans la console ; `GET /v1/key` est l’API de consultation en lecture seule de la clé courante. Chaque clé possède un identifiant immuable, un nom d’affichage, un secret masqué, un état, une date de création, une expiration facultative, un plafond de dépense et une attribution de l’utilisation. Le solde du compte et la marge restante de la clé doivent tous deux couvrir la requête. Les réservations non finalisées comptent dans les deux limites : les appels parallèles ne peuvent donc pas dépasser le budget en se fondant sur des totaux périmés. Une clé sans plafond nécessite toujours des fonds sur le compte. Abaisser un plafond sous le montant déjà consommé ou réservé empêche de nouveaux traitements ; cela n’efface pas les dépenses antérieures et n’annule pas les tâches acceptées. La suspension refuse les nouveaux appels et peut être levée. La révocation refuse définitivement les nouveaux appels. Une clé expirée ne peut pas appeler l’API, même si son propriétaire dispose de fonds. Les tâches existantes continuent et conservent l’identifiant de la clé qui les a soumises. Une clé de remplacement commence un historique d’utilisation distinct. Le Playground s’authentifie via la session connectée et apparaît séparément avec la source `playground` ; il n’expose jamais la clé permanente de l’utilisateur et n’en exige pas la saisie. Les graphiques du compte agrègent l’utilisation du Playground et de l’API externe. Les graphiques filtrés par clé ne montrent que cette clé ; les clés archivées ou révoquées restent disponibles dans les filtres historiques. Le serveur renvoie le secret en clair une seule fois ; les consultations ultérieures ne permettent pas de le récupérer. [Authentification](/docs?page=auth&lang=fr) · [Facturation](/docs?page=billing&lang=fr) ## Champs de restriction Chaque clé sélectionne un plafond de dépense et une période : cumulée, quotidienne, hebdomadaire ou mensuelle. Vous pouvez modifier la période ; les dépenses définitivement facturées qui lui correspondent sont recalculées pour la période courante, sans supprimer l’historique. Les périodes quotidienne, hebdomadaire et mensuelle sont réinitialisées respectivement à minuit UTC, le lundi à 00:00 UTC et le premier jour du mois à 00:00 UTC. Toutes les réservations non finalisées comptent dans le plafond actif, y compris celles acceptées avant un changement de période : les traitements en cours ne peuvent ainsi pas contourner une limite nouvellement réinitialisée. Une fois le montant finalisé, la dépense est rattachée à la date d’acceptation de la requête ; la période cumulée comprend tout l’historique. Les listes de modèles autorisés, le RPM et les listes d’adresses IPv4/IPv6 ou de plages CIDR ajoutent des contrôles indépendants. Une liste de modèles ou d’IP vide signifie qu’aucune restriction de ce type ne s’applique ; l’interface l’indique explicitement. Les deux familles d’adresses sont prises en charge. Les plafonds de la plateforme au niveau du compte restent applicables. La marge restante d’une clé n’est pas le solde du compte. En production, l’application des restrictions nécessite une configuration correcte des proxys de confiance. GET /v1/key renvoie id, name, mask, enabled, revoked, limit, period, expiresAt, models, rpm, ips, spent, reserved et nextReset. Les champs monétaires des clés utilisent des nombres exprimés en USD. limit=null signifie l’absence de plafond pour la clé ; limit=0 n’autorise aucune dépense positive. La contrainte du solde du compte reste applicable. Aucun secret n’est renvoyé. --- # Erreurs et limites ```json {"error":{"code":"KEY_BUDGET_EXCEEDED","message":"This key has insufficient spending allowance.","request_id":"req_demo_error"}} ``` | HTTP | Code | Mesure à prendre | |---|---|---| | 422 | `INVALID_INPUT` | Corrigez l’entrée avant de renvoyer la requête. | | 401 | `INVALID_API_KEY`, `API_KEY_INACTIVE` | Utilisez des identifiants valides. | | 403 | `MODEL_NOT_ALLOWED`, `IP_NOT_ALLOWED` | Vérifiez les restrictions de la clé dans la console. | | 402 | `INSUFFICIENT_BALANCE`, `KEY_BUDGET_EXCEEDED` | Rechargez le compte ou modifiez le budget autorisé. | | 404 | `MODEL_NOT_FOUND`, `REQUEST_NOT_FOUND` | Vérifiez l’identifiant et l’appartenance au compte. | | 409 | `IDEMPOTENCY_CONFLICT` | Utilisez le corps d’origine ou créez une nouvelle requête logique. | | 409 | Enregistrement d’une requête en cours ou `RESPONSE_CACHE_EXPIRED` | Attendez ou effectuez un rapprochement ; ne payez pas une génération en double. | | 422 | `UNSUPPORTED_PARAMETER` | Corrigez le choix du modèle ou de l’entrée. | | 429 | `RATE_LIMITED` | Respectez Retry-After et réduisez le trafic. | | 502/503 | `MODEL_UNAVAILABLE` | Vérifiez l’état de la requête avant de tenter une nouvelle soumission. | Les messages sont expurgés des informations sensibles ; le solde du fournisseur, les secrets du marchand et les traces de pile brutes de l’amont ne sont jamais affichés. `X-Request-ID` facilite le diagnostic. Les erreurs amont inconnues ne doivent pas être qualifiées à tort de violations de la politique de contenu. Une fois les en-têtes SSE envoyés, une erreur est transmise dans un événement d’erreur SSE, puis le flux est fermé ; elle ne produit pas une seconde réponse HTTP. [Idempotence](/docs?page=tasks&lang=fr) · [Production](/docs?page=production&lang=fr) --- # Intégration en production Cette référence accompagne l’implémentation du service ; le déploiement et la validation de l’intégration sont des étapes distinctes. Avant d’envoyer du trafic réel, validez chaque route amont, schéma de paramètres, calcul de prix, relevé d’utilisation en streaming, signature de paiement et procédure de reprise sur incident dans un environnement de test séparé. - Utilisez des clés côté serveur, TLS, des délais d’attente explicites, un polling borné et des identifiants de requête. - Conservez les enregistrements d’idempotence avant la soumission ; un POST amont payant ne doit jamais être réessayé aveuglément après un incident de transport au résultat ambigu. - Gérez le grand livre du portefeuille, les réservations, les budgets des clés et la comptabilisation des paiements dans des transactions MySQL. Utilisez DynamoDB pour les projections des événements de requête et leur consultation, ainsi que pour les journaux de requêtes conservés 30 jours, sans en faire une seconde source de vérité pour le portefeuille. - Soumettez directement après avoir persisté la réservation et la prise en charge de l’envoi. Ne renvoyez une tâche asynchrone qu’après réception de son identifiant amont. Le GET de la tâche interroge son état et finalise la facturation ; un processus périodique de rapprochement vérifie les tâches non consultées et réessaie les écritures de journaux en échec. - Les journaux par défaut contiennent uniquement des métadonnées et expirent après 30 jours. N’y conservez ni prompts, ni contenu des conversations, ni fichiers d’entrée ou générés, ni en-têtes Authorization, ni secrets de paiement. - Le grand livre et les enregistrements de paiement suivent une politique de conservation distincte ; la suppression des métadonnées des requêtes ne doit pas supprimer l’historique financier. Les exigences de conservation en production doivent être définies avant la mise en service. - Déclenchez des alertes sur l’accumulation des rapprochements en attente, les réservations bloquées, les tentatives de comptabilisation en double, la latence des tâches, l’accumulation des écritures de journaux à réessayer, les erreurs amont et les écarts de paiement. Avant la mise en service, configurez les identifiants, DNS/TLS et permissions de charge de travail, puis validez les intégrations réelles. [Tâches](/docs?page=tasks&lang=fr) · [Facturation](/docs?page=billing&lang=fr) MySQL gère la coordination des tâches, les transactions financières et les enregistrements d’écritures de journaux à réessayer. Écrivez les métadonnées directement dans DynamoDB avec des projections par compte, clé et date, des partitions journalières et LOG_SHARDS ; excluez les enregistrements expirés lors des lectures. Un processus léger de rapprochement interroge les tâches non consultées et réessaie les écritures de journaux. Web, gateway et reconciler sont déployés séparément. Une réponse réussie renvoie l’URL originale du résultat sans modification. L’hôte original contrôle sa disponibilité, son expiration et les restrictions interorigines. Enregistrez le résultat avant expiration ; consulter une tâche terminée ne renouvelle pas le lien. Les métadonnées et URL peuvent être conservées pour la reprise, jamais les fichiers générés. --- # URL de référence et entrées temporaires Lorsque les métadonnées du modèle prennent en charge `image`, fournissez une URL HTTPS d’image de référence au premier niveau d’une requête d’image ou de vidéo. Le service ne peut pas accéder aux URL `blob:` du navigateur. La requête normalisée actuelle ne prend pas en charge plusieurs images de référence. Pour un modèle acceptant une image de référence, transmettez une URL HTTPS publique dans `image`. L’API publique d’inférence accepte des URL, pas les octets des fichiers. Dans Playground, seuls les utilisateurs connectés et vérifiés peuvent téléverser des images ou vidéos temporaires vers un stockage R2 privé. XXXRouter ne télécharge, ne stocke et ne réhéberge jamais les résultats générés. Les entrées temporaires acceptent PNG/JPEG/WebP jusqu’à 10 MiB et MP4/WebM jusqu’à 50 MiB, avec 20 tentatives par minute et par compte. Les liens de lecture durent au maximum 1 heure et ne sont pas renouvelés. Une règle d’expiration d’un jour supprime les objets de façon asynchrone, sans garantir leur suppression physique exactement après 24 heures. Les URL d’images peuvent servir de référence aux modèles compatibles. Les URL vidéo nécessitent un modèle déclarant une entrée vidéo et ne peuvent pas servir d’image de référence. Les URL externes doivent rester accessibles pendant le traitement. ```json {"model":"seedream-4.5","prompt":"A small blue house","image":"https://example.com/reference.png","n":1,"size":"2048x2048"} ``` Une réponse réussie renvoie l’URL originale du résultat sans modification. L’hôte original contrôle sa disponibilité, son expiration et les restrictions interorigines. Enregistrez le résultat avant expiration ; consulter une tâche terminée ne renouvelle pas le lien. Les métadonnées et URL peuvent être conservées pour la reprise, jamais les fichiers générés. --- # Python — guide d’intégration Exécutez ces exemples sur votre serveur. Ils utilisent le client HTTP de l’environnement d’exécution du langage, et non un SDK de la plateforme. Aucune requête n’est envoyée avant l’exécution d’une commande. Les modèles doivent être configurés et activés avant utilisation. ## Préparation Environnement d’exécution: **Python 3.11+**. [Télécharger le code source complet](/examples/python.py). Définissez l’URL de base de votre déploiement, en y incluant `/v1` une seule fois, ainsi qu’une clé API côté serveur. `https://xxxrouter.com` est le domaine officiel du service ; pour les tests locaux, remplacez-le par l’origine de votre déploiement. Le service n’est pas encore déployé et la validation de l’intégration n’est pas terminée. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Exécuter un mode `chat` lit les deltas SSE et l’utilisation finale ; `image` reçoit une image synchrone ; `async-image` et `video` traitent HTTP 202 et consultent la tâche renvoyée. Le polling est limité à environ cinq minutes, auxquelles peut s’ajouter la durée d’une lecture déjà en cours ; un dépassement de délai conserve la tâche pour des lectures ultérieures. ```sh python3 python.py chat # python3 python.py image # python3 python.py async-image # python3 python.py video ``` ## Idempotence et gestion des échecs Une nouvelle exécution crée une nouvelle clé d’idempotence et peut entraîner une nouvelle dépense. Dans une application, conservez cette clé avec le corps exact de la requête avant de l’envoyer. Ne définissez `XXXROUTER_REQUEST_ID` que pour reprendre la même requête logique avec le même modèle, le même corps, la même clé API et la même source. Ne relancez pas avec une nouvelle clé après un échec de transport. Ces clients effectuent un seul POST de génération et ne le réessaient jamais automatiquement. Conservez le `X-Request-ID` de la réponse. En cas d’erreur HTTP, le corps de la réponse est conservé pour le diagnostic. Une tâche avec `status=unknown` arrête le polling automatique et nécessite une lecture ou un rapprochement ultérieur ; il ne s’agit ni d’une requête réussie ni d’une requête gratuite. Un événement d’erreur dans le flux ou l’absence de `[DONE]` indique une interruption. L’absence de données d’utilisation ne signifie pas un coût nul. Fermer la connexion n’annule pas les traitements acceptés. Ne consignez pas les clés API ni les URL signées des médias dans les journaux. ## Images d’entrée et fichiers de résultat Pour un modèle acceptant une image de référence, transmettez une URL HTTPS publique dans `image`. L’API publique d’inférence accepte des URL, pas les octets des fichiers. Dans Playground, seuls les utilisateurs connectés et vérifiés peuvent téléverser des images ou vidéos temporaires vers un stockage R2 privé. XXXRouter ne télécharge, ne stocke et ne réhéberge jamais les résultats générés. Une réponse réussie renvoie l’URL originale du résultat sans modification. L’hôte original contrôle sa disponibilité, son expiration et les restrictions interorigines. Enregistrez le résultat avant expiration ; consulter une tâche terminée ne renouvelle pas le lien. Les métadonnées et URL peuvent être conservées pour la reprise, jamais les fichiers générés. ## Exemple complet Ce code est un exemple d’intégration, pas la preuve qu’un modèle réel ou un canal de paiement est activé. Ne l’intégrez pas à un bundle destiné au navigateur. ```python """Python 3.11+, standard library only. No automatic generation retries.""" import json import os import sys import time import urllib.error import urllib.request import uuid BASE = os.environ['XXXROUTER_BASE_URL'].rstrip('/') # Include /v1 exactly once. KEY = os.environ['XXXROUTER_API_KEY'] MODE = sys.argv[1] if len(sys.argv) > 1 else 'chat' CASES = { 'chat': ('chat/completions', {'model': 'deepseek-v4-flash', 'messages': [{'role': 'user', 'content': 'Explain HTTP in one sentence.'}], 'max_tokens': 256, 'stream': True, 'stream_options': {'include_usage': True}}), 'image': ('images/generations', {'model': 'seedream-4.5', 'prompt': 'A ceramic cup in natural light', 'size': '2048x2048', 'n': 1}), 'async-image': ('images/generations', {'model': 'nano-banana-pro', 'prompt': 'A ceramic cup in natural light', 'size': '1024x1024', 'n': 1}), 'video': ('videos/generations', {'model': 'kling-3.0', 'prompt': 'A slow camera move across a mountain landscape', 'duration': 5, 'aspect_ratio': '16:9'}), } class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None opener = urllib.request.build_opener(NoRedirect) def request(path, payload=None, idem=None): headers = {'Authorization': 'Bearer ' + KEY} if idem: headers['Idempotency-Key'] = idem data = None if payload is not None: headers['Content-Type'] = 'application/json' data = json.dumps(payload).encode() req = urllib.request.Request(BASE + '/' + path, data=data, headers=headers) try: return opener.open(req, timeout=300 if payload is not None else 30) except urllib.error.HTTPError as error: # Read the structured failure: an uncertain request may already have a hold. print('HTTP', error.code, 'request_id=', error.headers.get('X-Request-ID'), file=sys.stderr) print(error.read().decode(), file=sys.stderr) raise def stream(response): event = [] def consume(): data = '\n'.join(event) if data == '[DONE]': return True if data: chunk = json.loads(data) if chunk.get('error'): raise RuntimeError(chunk['error']) for choice in chunk.get('choices', []): print(choice.get('delta', {}).get('content') or '', end='', flush=True) if chunk.get('usage'): print('\nusage:', chunk['usage']) return False for raw in response: line = raw.decode('utf-8').rstrip('\r\n') if line == '': if consume(): return event.clear() elif line.startswith('data:'): event.append(line[5:].lstrip(' ')) if event and consume(): return raise RuntimeError('Stream interrupted. Keep request ID and idempotency key; do not create another submission.') def finish(result): deadline = time.monotonic() + 300 while result.get('task_id') and result.get('status') not in ('succeeded', 'failed', 'unknown'): if time.monotonic() >= deadline: print(result) raise TimeoutError('Keep task_id and query later; do not resubmit.') time.sleep(2) with request('tasks/' + result['task_id']) as response: result = json.load(response) print(json.dumps(result, indent=2)) if result.get('status') == 'unknown': raise RuntimeError('Outcome unconfirmed. Retain IDs and wait for reconciliation; do not resubmit.') if result.get('status') == 'failed': raise RuntimeError('Generation failed. Inspect error and billing before a new logical request.') if __name__ == '__main__': path, payload = CASES[MODE] idem = os.environ.get('XXXROUTER_REQUEST_ID') or str(uuid.uuid4()) print('idempotency_key=', idem, file=sys.stderr) # Persist the key with this exact payload before submitting in a real application. with request(path, payload, idem) as response: print('request_id=', response.headers.get('X-Request-ID'), file=sys.stderr) if 'text/event-stream' in response.headers.get('Content-Type', ''): stream(response) else: finish(json.load(response)) ``` ## Références associées [chat](/docs?page=chat&lang=fr) · [images](/docs?page=images&lang=fr) · [videos](/docs?page=videos&lang=fr) · [tasks](/docs?page=tasks&lang=fr) · [uploads](/docs?page=uploads&lang=fr) · [errors](/docs?page=errors&lang=fr) [Documentation de l’environnement d’exécution](https://docs.python.org/3/library/urllib.request.html) · 2026-10-26 --- # TypeScript / Node.js — guide d’intégration Exécutez ces exemples sur votre serveur. Ils utilisent le client HTTP de l’environnement d’exécution du langage, et non un SDK de la plateforme. Aucune requête n’est envoyée avant l’exécution d’une commande. Les modèles doivent être configurés et activés avant utilisation. ## Préparation Environnement d’exécution: **Node.js 22.18+**. [Télécharger le code source complet](/examples/typescript.ts). Définissez l’URL de base de votre déploiement, en y incluant `/v1` une seule fois, ainsi qu’une clé API côté serveur. `https://xxxrouter.com` est le domaine officiel du service ; pour les tests locaux, remplacez-le par l’origine de votre déploiement. Le service n’est pas encore déployé et la validation de l’intégration n’est pas terminée. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Exécuter un mode `chat` lit les deltas SSE et l’utilisation finale ; `image` reçoit une image synchrone ; `async-image` et `video` traitent HTTP 202 et consultent la tâche renvoyée. Le polling est limité à environ cinq minutes, auxquelles peut s’ajouter la durée d’une lecture déjà en cours ; un dépassement de délai conserve la tâche pour des lectures ultérieures. ```sh node --experimental-strip-types typescript.ts chat # node --experimental-strip-types typescript.ts image # node --experimental-strip-types typescript.ts async-image # node --experimental-strip-types typescript.ts video ``` ## Idempotence et gestion des échecs Une nouvelle exécution crée une nouvelle clé d’idempotence et peut entraîner une nouvelle dépense. Dans une application, conservez cette clé avec le corps exact de la requête avant de l’envoyer. Ne définissez `XXXROUTER_REQUEST_ID` que pour reprendre la même requête logique avec le même modèle, le même corps, la même clé API et la même source. Ne relancez pas avec une nouvelle clé après un échec de transport. Ces clients effectuent un seul POST de génération et ne le réessaient jamais automatiquement. Conservez le `X-Request-ID` de la réponse. En cas d’erreur HTTP, le corps de la réponse est conservé pour le diagnostic. Une tâche avec `status=unknown` arrête le polling automatique et nécessite une lecture ou un rapprochement ultérieur ; il ne s’agit ni d’une requête réussie ni d’une requête gratuite. Un événement d’erreur dans le flux ou l’absence de `[DONE]` indique une interruption. L’absence de données d’utilisation ne signifie pas un coût nul. Fermer la connexion n’annule pas les traitements acceptés. Ne consignez pas les clés API ni les URL signées des médias dans les journaux. ## Images d’entrée et fichiers de résultat Pour un modèle acceptant une image de référence, transmettez une URL HTTPS publique dans `image`. L’API publique d’inférence accepte des URL, pas les octets des fichiers. Dans Playground, seuls les utilisateurs connectés et vérifiés peuvent téléverser des images ou vidéos temporaires vers un stockage R2 privé. XXXRouter ne télécharge, ne stocke et ne réhéberge jamais les résultats générés. Une réponse réussie renvoie l’URL originale du résultat sans modification. L’hôte original contrôle sa disponibilité, son expiration et les restrictions interorigines. Enregistrez le résultat avant expiration ; consulter une tâche terminée ne renouvelle pas le lien. Les métadonnées et URL peuvent être conservées pour la reprise, jamais les fichiers générés. ## Exemple complet Ce code est un exemple d’intégration, pas la preuve qu’un modèle réel ou un canal de paiement est activé. Ne l’intégrez pas à un bundle destiné au navigateur. ```typescript // Node.js 22.18+; run: node --experimental-strip-types typescript.ts chat // Uses native fetch, including explicit HTTP and SSE error handling. import { randomUUID } from 'node:crypto'; type Result = { status?: string; task_id?: string; error?: unknown; [key: string]: any }; const base = process.env.XXXROUTER_BASE_URL?.replace(/\/$/, ''); // Include /v1. const key = process.env.XXXROUTER_API_KEY; if (!base || !key) throw new Error('Set XXXROUTER_BASE_URL and XXXROUTER_API_KEY'); const cases: Record]> = { chat: ['chat/completions', { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'Explain HTTP in one sentence.' }], max_tokens: 256, stream: true, stream_options: { include_usage: true } }], image: ['images/generations', { model: 'seedream-4.5', prompt: 'A ceramic cup in natural light', size: '2048x2048', n: 1 }], 'async-image': ['images/generations', { model: 'nano-banana-pro', prompt: 'A ceramic cup in natural light', size: '1024x1024', n: 1 }], video: ['videos/generations', { model: 'kling-3.0', prompt: 'A slow camera move across a mountain landscape', duration: 5, aspect_ratio: '16:9' }], }; async function request(path: string, payload?: unknown, idem?: string): Promise { const headers: Record = { Authorization: `Bearer ${key}` }; if (idem) headers['Idempotency-Key'] = idem; if (payload) headers['Content-Type'] = 'application/json'; const response = await fetch(`${base}/${path}`, { method: payload ? 'POST' : 'GET', headers, redirect: 'error', body: payload ? JSON.stringify(payload) : undefined, signal: AbortSignal.timeout(payload ? 300_000 : 30_000), }); console.error('request_id=', response.headers.get('X-Request-ID')); if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); return response; } async function stream(response: Response): Promise { if (!response.body) throw new Error('Empty response body'); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ''; let data: string[] = []; const consume = (): boolean => { const value = data.join('\n'); data = []; if (value === '[DONE]') return true; if (!value) return false; const chunk = JSON.parse(value); if (chunk.error) throw new Error(JSON.stringify(chunk.error)); for (const choice of chunk.choices ?? []) process.stdout.write(choice.delta?.content ?? ''); if (chunk.usage) console.log('\nusage:', chunk.usage); return false; }; try { while (true) { const { value, done } = await reader.read(); buffer += value ?? ''; let end: number; while ((end = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, end).replace(/\r$/, ''); buffer = buffer.slice(end + 1); if (!line && consume()) return; if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, '')); } if (done) break; } if (buffer.startsWith('data:')) data.push(buffer.slice(5).trimStart()); if (consume()) return; throw new Error('Stream interrupted. Keep request ID and idempotency key; do not resubmit.'); } finally { await reader.cancel(); reader.releaseLock(); } } async function finish(result: Result): Promise { const deadline = Date.now() + 300_000; while (result.task_id && !['succeeded', 'failed', 'unknown'].includes(result.status ?? '')) { if (Date.now() >= deadline) throw new Error(`Query task ${result.task_id} later; do not resubmit`); await new Promise(resolve => setTimeout(resolve, 2000)); result = await (await request(`tasks/${encodeURIComponent(result.task_id)}`)).json(); } console.log(result); if (result.status === 'unknown') throw new Error('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (result.status === 'failed') throw new Error('Generation failed; inspect error and billing.'); } const [path, payload] = cases[process.argv[2] ?? 'chat']; const idem = process.env.XXXROUTER_REQUEST_ID || randomUUID(); console.error('idempotency_key=', idem); // Persist alongside the exact payload before submitting. const response = await request(path, payload, idem); if (response.headers.get('content-type')?.includes('text/event-stream')) await stream(response); else await finish(await response.json()); ``` ## Références associées [chat](/docs?page=chat&lang=fr) · [images](/docs?page=images&lang=fr) · [videos](/docs?page=videos&lang=fr) · [tasks](/docs?page=tasks&lang=fr) · [uploads](/docs?page=uploads&lang=fr) · [errors](/docs?page=errors&lang=fr) [Documentation de l’environnement d’exécution](https://nodejs.org/api/globals.html#fetch) · 2026-10-26 --- # Go — guide d’intégration Exécutez ces exemples sur votre serveur. Ils utilisent le client HTTP de l’environnement d’exécution du langage, et non un SDK de la plateforme. Aucune requête n’est envoyée avant l’exécution d’une commande. Les modèles doivent être configurés et activés avant utilisation. ## Préparation Environnement d’exécution: **Go 1.22+**. [Télécharger le code source complet](/examples/go.go). Définissez l’URL de base de votre déploiement, en y incluant `/v1` une seule fois, ainsi qu’une clé API côté serveur. `https://xxxrouter.com` est le domaine officiel du service ; pour les tests locaux, remplacez-le par l’origine de votre déploiement. Le service n’est pas encore déployé et la validation de l’intégration n’est pas terminée. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Exécuter un mode `chat` lit les deltas SSE et l’utilisation finale ; `image` reçoit une image synchrone ; `async-image` et `video` traitent HTTP 202 et consultent la tâche renvoyée. Le polling est limité à environ cinq minutes, auxquelles peut s’ajouter la durée d’une lecture déjà en cours ; un dépassement de délai conserve la tâche pour des lectures ultérieures. ```sh go run go.go chat # go run go.go image # go run go.go async-image # go run go.go video ``` ## Idempotence et gestion des échecs Une nouvelle exécution crée une nouvelle clé d’idempotence et peut entraîner une nouvelle dépense. Dans une application, conservez cette clé avec le corps exact de la requête avant de l’envoyer. Ne définissez `XXXROUTER_REQUEST_ID` que pour reprendre la même requête logique avec le même modèle, le même corps, la même clé API et la même source. Ne relancez pas avec une nouvelle clé après un échec de transport. Ces clients effectuent un seul POST de génération et ne le réessaient jamais automatiquement. Conservez le `X-Request-ID` de la réponse. En cas d’erreur HTTP, le corps de la réponse est conservé pour le diagnostic. Une tâche avec `status=unknown` arrête le polling automatique et nécessite une lecture ou un rapprochement ultérieur ; il ne s’agit ni d’une requête réussie ni d’une requête gratuite. Un événement d’erreur dans le flux ou l’absence de `[DONE]` indique une interruption. L’absence de données d’utilisation ne signifie pas un coût nul. Fermer la connexion n’annule pas les traitements acceptés. Ne consignez pas les clés API ni les URL signées des médias dans les journaux. ## Images d’entrée et fichiers de résultat Pour un modèle acceptant une image de référence, transmettez une URL HTTPS publique dans `image`. L’API publique d’inférence accepte des URL, pas les octets des fichiers. Dans Playground, seuls les utilisateurs connectés et vérifiés peuvent téléverser des images ou vidéos temporaires vers un stockage R2 privé. XXXRouter ne télécharge, ne stocke et ne réhéberge jamais les résultats générés. Une réponse réussie renvoie l’URL originale du résultat sans modification. L’hôte original contrôle sa disponibilité, son expiration et les restrictions interorigines. Enregistrez le résultat avant expiration ; consulter une tâche terminée ne renouvelle pas le lien. Les métadonnées et URL peuvent être conservées pour la reprise, jamais les fichiers générés. ## Exemple complet Ce code est un exemple d’intégration, pas la preuve qu’un modèle réel ou un canal de paiement est activé. Ne l’intégrez pas à un bundle destiné au navigateur. ```go // Go 1.22+, standard library only. Run: go run go.go chat package main import ( "bufio" "bytes" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) type Object = map[string]any var base = strings.TrimRight(os.Getenv("XXXROUTER_BASE_URL"), "/") // Include /v1. var key = os.Getenv("XXXROUTER_API_KEY") var client = &http.Client{Timeout: 300 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} func request(path string, payload Object, idem string) (*http.Response, error) { var body io.Reader method := "GET" if payload != nil { raw, err := json.Marshal(payload) if err != nil { return nil, err } body = bytes.NewReader(raw) method = "POST" } req, err := http.NewRequest(method, base+"/"+path, body) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) if payload != nil { req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", idem) } response, err := client.Do(req) if err != nil { return nil, err } fmt.Fprintln(os.Stderr, "request_id=", response.Header.Get("X-Request-ID")) if response.StatusCode < 200 || response.StatusCode >= 300 { defer response.Body.Close() raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20)) return nil, fmt.Errorf("HTTP %d: %s", response.StatusCode, raw) } return response, nil } func decode(response *http.Response) (Object, error) { defer response.Body.Close() var result Object err := json.NewDecoder(response.Body).Decode(&result) return result, err } func stream(response *http.Response) error { defer response.Body.Close() scanner := bufio.NewScanner(response.Body) scanner.Buffer(make([]byte, 4096), 2<<20) var parts []string consume := func() (bool, error) { data := strings.Join(parts, "\n") parts = nil if data == "[DONE]" { return true, nil } if data == "" { return false, nil } var chunk struct { Error json.RawMessage `json:"error"` Usage json.RawMessage `json:"usage"` Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` } if err := json.Unmarshal([]byte(data), &chunk); err != nil { return false, err } if len(chunk.Error) > 0 && string(chunk.Error) != "null" { return false, fmt.Errorf("stream error: %s", chunk.Error) } for _, choice := range chunk.Choices { fmt.Print(choice.Delta.Content) } if len(chunk.Usage) > 0 && string(chunk.Usage) != "null" { fmt.Printf("\nusage: %s\n", chunk.Usage) } return false, nil } for scanner.Scan() { line := scanner.Text() if line == "" { done, err := consume() if err != nil || done { return err } } if strings.HasPrefix(line, "data:") { parts = append(parts, strings.TrimPrefix(line[5:], " ")) } } if err := scanner.Err(); err != nil { return err } done, err := consume() if err != nil || done { return err } return fmt.Errorf("stream interrupted; keep request ID and idempotency key, do not resubmit") } func finish(result Object) error { deadline := time.Now().Add(5 * time.Minute) for { task, hasTask := result["task_id"].(string) status, _ := result["status"].(string) if !hasTask || status == "succeeded" || status == "failed" || status == "unknown" { break } if time.Now().After(deadline) { return fmt.Errorf("query task %s later; do not resubmit", task) } time.Sleep(2 * time.Second) response, err := request("tasks/"+task, nil, "") if err != nil { return err } result, err = decode(response) if err != nil { return err } } raw, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(raw)) if result["status"] == "unknown" { return fmt.Errorf("outcome unconfirmed; retain IDs and wait for reconciliation") } if result["status"] == "failed" { return fmt.Errorf("generation failed; inspect error and billing") } return nil } func run() error { if base == "" || key == "" { return fmt.Errorf("set XXXROUTER_BASE_URL and XXXROUTER_API_KEY") } mode := "chat" if len(os.Args) > 1 { mode = os.Args[1] } path := "images/generations" var payload Object switch mode { case "chat": path = "chat/completions" payload = Object{"model": "deepseek-v4-flash", "messages": []Object{{"role": "user", "content": "Explain HTTP in one sentence."}}, "max_tokens": 256, "stream": true, "stream_options": Object{"include_usage": true}} case "image": payload = Object{"model": "seedream-4.5", "prompt": "A ceramic cup in natural light", "size": "2048x2048", "n": 1} case "async-image": payload = Object{"model": "nano-banana-pro", "prompt": "A ceramic cup in natural light", "size": "1024x1024", "n": 1} case "video": path = "videos/generations" payload = Object{"model": "kling-3.0", "prompt": "A slow camera move across a mountain landscape", "duration": 5, "aspect_ratio": "16:9"} default: return fmt.Errorf("choose chat, image, async-image or video") } idem := os.Getenv("XXXROUTER_REQUEST_ID") if idem == "" { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return err } idem = hex.EncodeToString(b) } fmt.Fprintln(os.Stderr, "idempotency_key=", idem) // Persist with the exact payload before submission. response, err := request(path, payload, idem) if err != nil { return err } if strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { return stream(response) } result, err := decode(response) if err != nil { return err } return finish(result) } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } ``` ## Références associées [chat](/docs?page=chat&lang=fr) · [images](/docs?page=images&lang=fr) · [videos](/docs?page=videos&lang=fr) · [tasks](/docs?page=tasks&lang=fr) · [uploads](/docs?page=uploads&lang=fr) · [errors](/docs?page=errors&lang=fr) [Documentation de l’environnement d’exécution](https://pkg.go.dev/net/http) · 2026-10-26 --- # PHP — guide d’intégration Exécutez ces exemples sur votre serveur. Ils utilisent le client HTTP de l’environnement d’exécution du langage, et non un SDK de la plateforme. Aucune requête n’est envoyée avant l’exécution d’une commande. Les modèles doivent être configurés et activés avant utilisation. ## Préparation Environnement d’exécution: **PHP 8.1+ + ext-curl**. [Télécharger le code source complet](/examples/php.php). Définissez l’URL de base de votre déploiement, en y incluant `/v1` une seule fois, ainsi qu’une clé API côté serveur. `https://xxxrouter.com` est le domaine officiel du service ; pour les tests locaux, remplacez-le par l’origine de votre déploiement. Le service n’est pas encore déployé et la validation de l’intégration n’est pas terminée. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Exécuter un mode `chat` lit les deltas SSE et l’utilisation finale ; `image` reçoit une image synchrone ; `async-image` et `video` traitent HTTP 202 et consultent la tâche renvoyée. Le polling est limité à environ cinq minutes, auxquelles peut s’ajouter la durée d’une lecture déjà en cours ; un dépassement de délai conserve la tâche pour des lectures ultérieures. ```sh php php.php chat # php php.php image # php php.php async-image # php php.php video ``` ## Idempotence et gestion des échecs Une nouvelle exécution crée une nouvelle clé d’idempotence et peut entraîner une nouvelle dépense. Dans une application, conservez cette clé avec le corps exact de la requête avant de l’envoyer. Ne définissez `XXXROUTER_REQUEST_ID` que pour reprendre la même requête logique avec le même modèle, le même corps, la même clé API et la même source. Ne relancez pas avec une nouvelle clé après un échec de transport. Ces clients effectuent un seul POST de génération et ne le réessaient jamais automatiquement. Conservez le `X-Request-ID` de la réponse. En cas d’erreur HTTP, le corps de la réponse est conservé pour le diagnostic. Une tâche avec `status=unknown` arrête le polling automatique et nécessite une lecture ou un rapprochement ultérieur ; il ne s’agit ni d’une requête réussie ni d’une requête gratuite. Un événement d’erreur dans le flux ou l’absence de `[DONE]` indique une interruption. L’absence de données d’utilisation ne signifie pas un coût nul. Fermer la connexion n’annule pas les traitements acceptés. Ne consignez pas les clés API ni les URL signées des médias dans les journaux. ## Images d’entrée et fichiers de résultat Pour un modèle acceptant une image de référence, transmettez une URL HTTPS publique dans `image`. L’API publique d’inférence accepte des URL, pas les octets des fichiers. Dans Playground, seuls les utilisateurs connectés et vérifiés peuvent téléverser des images ou vidéos temporaires vers un stockage R2 privé. XXXRouter ne télécharge, ne stocke et ne réhéberge jamais les résultats générés. Une réponse réussie renvoie l’URL originale du résultat sans modification. L’hôte original contrôle sa disponibilité, son expiration et les restrictions interorigines. Enregistrez le résultat avant expiration ; consulter une tâche terminée ne renouvelle pas le lien. Les métadonnées et URL peuvent être conservées pour la reprise, jamais les fichiers générés. ## Exemple complet Ce code est un exemple d’intégration, pas la preuve qu’un modèle réel ou un canal de paiement est activé. Ne l’intégrez pas à un bundle destiné au navigateur. ```php getMessage(); } }; $curl = curl_init($base . '/' . $path); $options = [ CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $payload !== null ? 300 : 30, CURLOPT_HEADERFUNCTION => function ($handle, string $line) use (&$contentType, &$requestId): int { if (stripos($line, 'Content-Type:') === 0) $contentType = trim(substr($line, 13)); if (stripos($line, 'X-Request-ID:') === 0) $requestId = trim(substr($line, 13)); return strlen($line); }, CURLOPT_WRITEFUNCTION => function ($handle, string $bytes) use (&$contentType, &$raw, &$buffer, &$data, &$done, &$streamError, $consume): int { if (!str_contains($contentType, 'text/event-stream')) { $raw .= $bytes; return strlen($bytes); } $buffer .= $bytes; while (($end = strpos($buffer, "\n")) !== false) { $line = rtrim(substr($buffer, 0, $end), "\r"); $buffer = substr($buffer, $end + 1); if ($line === '') $consume(); elseif (str_starts_with($line, 'data:')) $data[] = ltrim(substr($line, 5), ' '); } return strlen($bytes); }, ]; if ($payload !== null) { $headers[] = 'Content-Type: application/json'; $headers[] = 'Idempotency-Key: ' . $idem; $options[CURLOPT_POST] = true; $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_THROW_ON_ERROR); } $options[CURLOPT_HTTPHEADER] = $headers; curl_setopt_array($curl, $options); try { $ok = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); fwrite(STDERR, 'request_id=' . $requestId . "\n"); if ($ok === false) throw new RuntimeException('Transport error; retain IDs, do not resubmit: ' . curl_error($curl)); if ($status < 200 || $status >= 300) throw new RuntimeException('HTTP ' . $status . ': ' . $raw); if (str_contains($contentType, 'text/event-stream')) { if (str_starts_with($buffer, 'data:')) $data[] = ltrim(substr($buffer, 5), ' '); $consume(); if ($streamError !== null) throw new RuntimeException($streamError); if (!$done) throw new RuntimeException('Stream interrupted; retain request ID and idempotency key.'); return ['stream_complete' => true]; } return json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } finally { curl_close($curl); } } $cases = [ 'chat' => ['chat/completions', ['model'=>'deepseek-v4-flash','messages'=>[['role'=>'user','content'=>'Explain HTTP in one sentence.']],'max_tokens'=>256,'stream'=>true,'stream_options'=>['include_usage'=>true]]], 'image' => ['images/generations', ['model'=>'seedream-4.5','prompt'=>'A ceramic cup in natural light','size'=>'2048x2048','n'=>1]], 'async-image' => ['images/generations', ['model'=>'nano-banana-pro','prompt'=>'A ceramic cup in natural light','size'=>'1024x1024','n'=>1]], 'video' => ['videos/generations', ['model'=>'kling-3.0','prompt'=>'A slow camera move across a mountain landscape','duration'=>5,'aspect_ratio'=>'16:9']], ]; $mode = $argv[1] ?? 'chat'; if (!isset($cases[$mode])) throw new InvalidArgumentException('Choose chat, image, async-image or video'); [$path, $payload] = $cases[$mode]; $idem = getenv('XXXROUTER_REQUEST_ID') ?: bin2hex(random_bytes(16)); fwrite(STDERR, 'idempotency_key=' . $idem . "\n"); // Persist alongside exact payload before submission. $result = request($path, $payload, $idem); $deadline = microtime(true) + 300; while (isset($result['task_id']) && !in_array($result['status'], ['succeeded', 'failed', 'unknown'], true)) { if (microtime(true) >= $deadline) throw new RuntimeException('Query task ' . $result['task_id'] . ' later; do not resubmit.'); sleep(2); $result = request('tasks/' . rawurlencode($result['task_id'])); } echo json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . "\n"; if (($result['status'] ?? '') === 'unknown') throw new RuntimeException('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (($result['status'] ?? '') === 'failed') throw new RuntimeException('Generation failed; inspect error and billing.'); ``` ## Références associées [chat](/docs?page=chat&lang=fr) · [images](/docs?page=images&lang=fr) · [videos](/docs?page=videos&lang=fr) · [tasks](/docs?page=tasks&lang=fr) · [uploads](/docs?page=uploads&lang=fr) · [errors](/docs?page=errors&lang=fr) [Documentation de l’environnement d’exécution](https://www.php.net/manual/en/function.curl-exec.php) · 2026-10-26 --- # Historique des modifications ## 0.1.0 — 2026-10-26 — Projet d’implémentation - Ajout d’un solde de compte en USD et de contrôles des dépenses par clé. - Ajout de la référence développeur en anglais (en) et en chinois simplifié (zh-CN), ainsi que d’un projet OpenAPI lisible par machine. - Distinction entre les résultats directs d’images synchrones et les tâches asynchrones d’images ou de vidéos. - Définition du chat textuel de type OpenAI avec SSE facultatif. - Ajout d’un crédit d’essai de $0.20 valable 7 jours et des pages de paiement hébergées Payblis / NOWPayments. - Définition de la politique par défaut des journaux de requêtes : métadonnées uniquement, pendant 30 jours. Cette référence décrit des points d’accès implémentés. Le déploiement, la validation des routes réelles et l’acceptation des canaux de paiement restent des conditions de mise en service distinctes. [Démarrage rapide](/docs?page=quickstart&lang=fr) --- # Inicio rápido > Referencia de implementación · 2026-10-26 · Configura un despliegue antes de enviar tráfico. La aplicación gestiona la autenticación, los saldos y las solicitudes en el servidor. `https://xxxrouter.com` es el dominio oficial del servicio; para las pruebas locales, sustitúyelo por el origen de tu despliegue. El servicio aún no se ha desplegado y la certificación de integración no está completa. La pasarela también admite `/v1` en el mismo origen. Los modelos están desactivados inicialmente; los operadores deben configurar las credenciales, certificar las rutas y establecer los precios antes de activarlos. Esta referencia no demuestra que se haya realizado un despliegue ni completado las pruebas de integración. 1. Inicia sesión en la consola y consulta tu saldo en USD. 2. Crea una clave de API. Cópiala cuando se muestre por única vez y guárdala en el gestor de secretos de tu servidor. 3. Revisa el modo de respuesta y el precio efectivo del modelo. 4. Llama a la API desde tu backend. Sigue el ID de solicitud devuelto en Uso y registros. ```sh export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' curl https://xxxrouter.com/v1/models \ -H "Authorization: Bearer $XXXROUTER_API_KEY" ``` ```sh curl https://xxxrouter.com/v1/chat/completions \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-chat-request-001' \ -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Hello"}],"max_tokens":256,"stream":false}' ``` El chat sigue un contrato de estilo OpenAI. Las API de imágenes admiten tanto resultados directos como respuestas de tareas; no todas son sustitutos directos de OpenAI Images. El Playground de la consola utiliza los mismos precios y la misma política contable que las llamadas externas a la API, mediante la sesión iniciada, con una atribución separada de las claves de API. Continúa con: [autenticación](/docs?page=auth&lang=es), [capacidades de los modelos](/docs?page=models&lang=es), [imágenes](/docs?page=images&lang=es), [errores](/docs?page=errors&lang=es). Interfaces para máquinas: [OpenAPI](/openapi.json), [llms.txt](/llms.txt), [texto completo](/llms-full.txt). --- # Autenticación Envía `Authorization: Bearer ` en las solicitudes de generación, tareas y consulta de claves. Los endpoints GET del catálogo de modelos permiten consultar precios públicos sin autenticación; una clave válida opcional selecciona los precios de la cuenta. Nunca incluyas claves en cadenas de consulta, repositorios públicos, paquetes de cliente, capturas de pantalla compartidas ni almacenamiento del navegador. Crea una clave real desde una cuenta de consola autenticada y verificada. Una clave activa se muestra una sola vez al crearla. Las vistas posteriores muestran su prefijo/sufijo y su ID inmutable. El servidor almacena un resumen unidireccional con clave, no texto sin cifrar recuperable. La gestión de claves requiere una sesión de consola; una clave de inferencia no puede crear otras claves ni modificar la facturación de la cuenta. Para rotar una clave: crea una sustituta, despliégala, confirma el tráfico y después revoca la original. La pausa es reversible; la revocación es permanente. La caducidad se evalúa en UTC con cada nueva solicitud. Las tareas ya aceptadas conservan su facturación e historial después de una pausa, caducidad o revocación. Utiliza otra clave autorizada de la misma cuenta o la consola para consultarlas. `GET /v1/key` devuelve los metadatos de la clave actual, incluidas las restricciones efectivas y el límite de gasto restante. Nunca devuelve el secreto. Los errores no revelan si una clave probada pertenece a otra cuenta. [Controles de claves](/docs?page=keys&lang=es) · [Errores](/docs?page=errors&lang=es) --- # Modelos y precios `GET /v1/models` enumera los modelos disponibles para tu cuenta. `GET /v1/models/{id}` devuelve sus capacidades, parámetros, modo de respuesta, disponibilidad y precio efectivo. Los ID públicos de los modelos son nombres propios de la plataforma sin prefijos de enrutamiento; por ejemplo, `/v1/models/seedream-4.5`. Los metadatos de enrutamiento e infraestructura son internos y no se devuelven en la API pública de modelos. | ID de modelo de demostración | Salida | Modo de respuesta | |---|---|---| | `seedream-4.5` | Imagen | `sync` | | `nano-banana-pro` | Imagen | `async` | | `flux-2-pro` | Imagen | `async` | | `kling-3.0` | Vídeo | `async` | | `deepseek-v4-flash` | Texto | Modo de catálogo `stream`; JSON o SSE se selecciona mediante `stream` | Estos son ID públicos de modelos de la plataforma, no garantías de disponibilidad real configurada o certificada. El modo de respuesta de un modelo figura explícitamente en los metadatos del catálogo; nunca se deduce de una subcadena de la URL ni de la palabra clave `async` de un lenguaje de programación. Las capacidades del modelo se validan antes de reservar fondos o enviar la solicitud al proveedor. Los precios se expresan en USD. Las tarifas de imágenes dependen de los parámetros admitidos, las de vídeo dependen de la duración en esta versión y las de LLM distinguen entre tokens de entrada y de salida. Al aceptar una solicitud se almacena la versión del precio efectivo; las modificaciones posteriores del catálogo no la cambian. Las estimaciones no son costes liquidados. [Protocolo de imágenes](/docs?page=images&lang=es) · [Facturación](/docs?page=billing&lang=es) ## Prioridad del precio efectivo Aplica exactamente una regla coincidente, en este orden: excepción de usuario × modelo, excepción de grupo de precios × modelo, descuento predeterminado del grupo de precios y precio público. Las excepciones pueden ser un precio fijo o un descuento dentro del intervalo permitido para ese modelo. Las reglas no se acumulan. Los precios por debajo del coste requieren confirmación explícita del operador; si se desconoce el coste del proveedor, no debe mostrarse un margen inventado. Fija la regla seleccionada, las unidades y la versión al aceptar la solicitud. La lista `parameters` del modelo anuncia campos normalizados de nivel superior: las imágenes usan `size`, `n`, `image`; los vídeos usan `duration`, `aspect_ratio`, `image`; el chat usa `temperature`, `max_tokens`, `stream`, `stream_options`. No es un diccionario arbitrario de parámetros que se reenvían sin validar. Los ejemplos de Seedream 4.5 usan `2048x2048`; los límites de parámetros en producción deben verificarse para cada modelo. Las opciones de ejemplo de la interfaz no garantizan capacidades reales. El JSON del modelo utiliza name, modality (llm/image/video), mode/response_mode (stream/sync/async), parameters, sizes, supportsImage y enabled. pricing es un objeto con currency, price y outputPrice que admite null (cadenas de USD con seis decimales); price/outputPrice son campos numéricos en USD. No se puede llamar a un modelo desactivado. Los precios iniciales requieren revisión del operador; los costes desconocidos nunca se presentan como márgenes. --- # Completaciones de chat `POST /v1/chat/completions` acepta `model`, `messages`, `max_tokens`, `temperature`, `stream` y, opcionalmente, `stream_options: {"include_usage": true}`. El contrato v0.1 cubre mensajes de texto con los roles `system`, `user` y `assistant`. Las llamadas a herramientas, el chat multimodal, los embeddings y la Responses API quedan fuera del contrato de esta versión. ```json {"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Explain HTTP in one sentence."}],"max_tokens":256,"temperature":0.7,"stream":true,"stream_options":{"include_usage":true}} ``` Un resultado sin streaming contiene `id`, `object: "chat.completion"`, `model`, `choices` y `usage`. El streaming devuelve `text/event-stream`; cada evento data contiene un fragmento de completación con `choices[].delta`. Con `stream_options.include_usage=true`, el evento final de uso del servicio de origen, cuando se proporciona, precede a `data: [DONE]`. Registra `X-Request-ID` de las cabeceras de respuesta. No interpretes SSE como un único documento JSON. La pasarela no debe insertar un mensaje de sistema que fuerce una salida exclusivamente JSON, cambiar de modelo sin avisar ni repetir una completación facturada para corregir la salida. Desconectar o pulsar Detener no elimina los tokens ya generados. La facturación final utiliza el uso observado; si falta un dato de uso autoritativo, queda pendiente de conciliación y nunca se convierte silenciosamente en cero. Cada ruta requiere certificación de integración antes de activarse. [Facturación](/docs?page=billing&lang=es) · [Producción](/docs?page=production&lang=es) Todas las solicitudes POST de generación requieren un Idempotency-Key (1–128 caracteres ASCII imprimibles). Utiliza un valor nuevo para una solicitud lógica nueva y el mismo valor únicamente para una repetición exacta. max_tokens admite 1–4096; temperature admite 0–2. La repetición de una solicitud de streaming no reinicia ni reproduce un stream ya facturado del servicio de origen. --- # Generación de imágenes `POST /v1/images/generations` tiene dos modos de respuesta declarados, seleccionados por el catálogo de modelos. Consulta siempre los metadatos del modelo y procesa la respuesta documentada. Ambos modos aceptan `model`, `prompt`, una URL de referencia `image` opcional de nivel superior, `size` y `n`. Son campos normalizados de la plataforma; los adaptadores del proveedor los convierten al esquema certificado del modelo. ```sh curl https://xxxrouter.com/v1/images/generations \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-image-request-001' \ -d '{"model":"seedream-4.5","prompt":"A ceramic cup in natural light","size":"2048x2048","n":1}' ``` ## Modelo síncrono: HTTP 200 ```json {"request_id":"req_demo_sync","model":"seedream-4.5","response_mode":"sync","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/cup.png"}],"billing":{"status":"settled","currency":"USD","amount":"0.030000"}} ``` La imagen llega en la respuesta HTTP original. No se necesita un ID de tarea del proveedor ni consultas posteriores de la tarea. El importe anterior es una demostración, no una cotización. Una ejecución síncrona lenta sigue siendo síncrona; un tiempo de espera agotado en el cliente no demuestra un fallo ni autoriza a volver a enviar la solicitud. ## Modelo asíncrono: HTTP 202 Selecciona `nano-banana-pro` o `flux-2-pro` para recibir: ```json {"request_id":"req_demo_async","task_id":"task_demo_01","model":"nano-banana-pro","response_mode":"async","status":"running"} ``` Consulta periódicamente [el endpoint de tareas](/docs?page=tasks&lang=es) hasta alcanzar un estado final. No interpretes `202` como una salida completada. Compartir una ruta de imágenes no implica que todos los modelos sean compatibles con OpenAI Images. Las rutas configuradas envían solicitudes reales. La ausencia de credenciales nunca genera un resultado de pago simulado. [Entradas de referencia](/docs?page=uploads&lang=es) · [Errores](/docs?page=errors&lang=es) El servicio actual solo admite n=1. image solo se permite cuando los parameters del modelo lo incluyen. Las respuestas correctas devuelven la URL original del resultado sin cambios. El host original controla su disponibilidad, caducidad y restricciones de origen. Guarda el resultado antes de que caduque; consultar una tarea completada no renueva la URL. Se pueden conservar metadatos y URL para recuperación, nunca los archivos generados. --- # Generación de vídeos `POST /v1/videos/generations` devuelve HTTP 202 y una tarea. El ID público del modelo es `kling-3.0`; la disponibilidad real requiere una ruta configurada y certificada. ```json {"model":"kling-3.0","prompt":"A slow camera movement around a ceramic cup","duration":5,"aspect_ratio":"16:9"} ``` Envía un `Idempotency-Key` para cada envío lógico. Guarda `task_id` y consulta periódicamente `/v1/tasks/{task_id}`. Los resultados correctos incluyen la URL del vídeo, el tipo de medio y la facturación final. No prometas cancelación ni un porcentaje de progreso del proveedor a menos que este lo admita realmente. El progreso exacto se omite si no hay datos fiables. Una duración cotizada es un dato para calcular el precio, no una garantía de la duración exacta del resultado. Antes del lanzamiento, cada ruta debe documentar cómo factura la duración de salida y las combinaciones de parámetros. No se presupone una sustitución automática por otro modelo. [Tareas](/docs?page=tasks&lang=es) · [Facturación](/docs?page=billing&lang=es) Los parámetros normalizados de nivel superior son `duration`, `aspect_ratio` y, opcionalmente, `image`. El esquema actual acepta 5/10 segundos y 16:9, 9:16 o 1:1; comprueba la disponibilidad del modelo antes de llamar. Este borrador de solicitud no tiene un campo `resolution` ni un campo `parameters` anidado. Las respuestas correctas devuelven la URL original del resultado sin cambios. El host original controla su disponibilidad, caducidad y restricciones de origen. Guarda el resultado antes de que caduque; consultar una tarea completada no renueva la URL. Se pueden conservar metadatos y URL para recuperación, nunca los archivos generados. --- # Tareas e idempotencia `GET /v1/tasks/{id}` consulta una tarea de imagen/vídeo aceptada. En cada consulta se comprueban la autenticación y la pertenencia a la cuenta. Otra clave válida de la misma cuenta puede consultar la tarea; el ID de la clave original sigue siendo la atribución inmutable del uso. Estados: `submitting → running → succeeded | failed`. Un resultado incierto utiliza `status: "unknown"` en la solicitud y `billing.status: "reconciling"`; no debe volver a enviarse automáticamente. La disponibilidad de una salida correcta y el estado de facturación son independientes: un resultado puede estar disponible mientras se concilia la liquidación. ```json {"task_id":"task_demo_01","request_id":"req_demo_async","model":"nano-banana-pro","response_mode":"async","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/image.png","media_type":"image"}],"billing":{"status":"settled","currency":"USD","amount":"0.150000"}} ``` Realiza consultas periódicas con un intervalo acotado (empieza en 2 segundos y aumenta progresivamente hasta 10 segundos); respeta `Retry-After`. Reintentar GET no crea trabajo nuevo. Perder la respuesta de un POST es distinto: reutiliza la misma clave de idempotencia y una carga idéntica. La misma cuenta + endpoint + clave + huella de la solicitud devuelve el registro original; una carga diferente devuelve `409 IDEMPOTENCY_CONFLICT`. Los registros de idempotencia se guardan de forma persistente en MySQL. No supongas que un cambio de día hace seguro reutilizar una clave antigua o volver a enviar una solicitud de resultado incierto. En imágenes síncronas, la repetición devuelve el resultado directo registrado cuando está disponible. Las solicitudes síncronas en curso o inciertas devuelven HTTP `409` con el registro actual de la solicitud; no se realiza un segundo envío al servicio de origen. Los ID de solicitudes síncronas no deben consultarse mediante `/tasks` como si fueran ID de tareas. [Errores](/docs?page=errors&lang=es) · [Producción](/docs?page=production&lang=es) Las respuestas correctas devuelven la URL original del resultado sin cambios. El host original controla su disponibilidad, caducidad y restricciones de origen. Guarda el resultado antes de que caduque; consultar una tarea completada no renueva la URL. Se pueden conservar metadatos y URL para recuperación, nunca los archivos generados. --- # Saldo y facturación La cuenta tiene un único saldo prepago en USD. Los límites de gasto de las claves restringen el consumo; no son monederos separados. Muestra por separado los importes disponibles, retenidos y liquidados. Almacena los importes monetarios como microdólares enteros o decimales exactos, nunca como números binarios de coma flotante. La prueba aprobada consiste en $0.20 de crédito promocional, válido durante 7 días. Está separado de los fondos pagados y no es dinero en efectivo. El servidor aplica los criterios de elegibilidad y la concesión única. Verifica tu correo electrónico y completa el desafío; la caducidad del crédito se aplica de forma independiente a los fondos pagados. Antes de aceptar trabajo, reserva de forma atómica fondos suficientes de la cuenta y del límite de la clave. Al finalizar, liquida el importe real y libera la diferencia. Los fallos sin uso facturable liberan la retención. Los resultados desconocidos del proveedor permanecen en conciliación en lugar de generar un segundo cargo. Un tiempo de espera agotado o la interrupción de un stream no demuestran un uso nulo. Payblis y NOWPayments son los únicos proveedores de pago incluidos. Los canales de pago configurados crean pedidos con páginas de pago alojadas; si faltan credenciales, devuelven un estado no disponible. Una redirección de éxito nunca abona fondos a una cuenta. Las notificaciones verificadas y la conciliación independiente deben comprobar la moneda, el importe, la referencia del proveedor y la identidad del pedido antes de registrar un asiento único en el libro mayor. Los estados de pedido implementados son `creating`, `pending`, `confirming`, `paid`, `underpaid`, `expired` y `failed`. Los pagos parciales permanecen en `underpaid` hasta que se confirme el pago completo. Esta versión no implementa una API de reembolsos ni el procesamiento automático de reembolsos. Cualquier reembolso futuro deberá añadir asientos compensatorios al libro mayor en lugar de reescribir transacciones históricas. [Controles de claves](/docs?page=keys&lang=es) · [Producción](/docs?page=production&lang=es) ## Elegibilidad de la prueba y orden de consumo Verifica el correo electrónico de la cuenta y completa el desafío de verificación antes de solicitar el crédito de $0.20. La solicitud es idempotente y está sujeta a límites de frecuencia. El crédito caduca exactamente 7 días después de su emisión; los fondos promocionales válidos se consumen antes que los fondos pagados. Las solicitudes de prueba utilizan el mismo precio efectivo y los mismos límites de clave/cuenta. Las retenciones conservan la atribución del origen de los fondos. Si una retención se libera después de que caduque el crédito, su parte promocional vuelve al saldo promocional caducado, no al saldo pagado disponible. La caducidad nunca elimina dinero pagado. El historial del libro mayor financiero es permanente e independiente de los registros de solicitudes de 30 días. --- # Controles de claves de API Gestiona las claves en la consola; `GET /v1/key` es la API de consulta de la propia clave, de solo lectura. Cada clave tiene un ID inmutable, nombre visible, secreto enmascarado, estado, fecha de creación, caducidad opcional, límite de gasto y atribución del uso. Tanto el saldo de la cuenta como el límite de la clave deben cubrir la solicitud. Las retenciones pendientes cuentan para ambos, de modo que las llamadas paralelas no pueden gastar de más al consultar totales desactualizados. Una clave sin límite sigue requiriendo fondos en la cuenta. Reducir un tope por debajo del importe ya ocupado impide aceptar trabajo nuevo; no elimina el gasto anterior ni cancela las tareas aceptadas. La pausa rechaza llamadas nuevas y se puede revertir. La revocación rechaza llamadas nuevas de forma permanente. Una clave caducada no puede llamar a la API aunque su propietario tenga fondos. Las tareas existentes continúan y conservan el ID de la clave que las envió. Una clave de sustitución inicia un historial de uso distinto. Playground se autentica mediante la sesión iniciada y se muestra por separado como origen `playground`; nunca expone ni requiere la clave permanente del usuario. Los gráficos de la cuenta agregan el uso de Playground y de la API externa. Los gráficos filtrados por clave muestran solo esa clave; las claves archivadas o revocadas siguen disponibles en los filtros históricos. El servidor devuelve el secreto en texto sin cifrar una sola vez; las vistas posteriores no pueden recuperarlo. [Autenticación](/docs?page=auth&lang=es) · [Facturación](/docs?page=billing&lang=es) ## Campos de restricción Cada clave selecciona un tope de gasto y un periodo: lifetime, daily, weekly o monthly. Puedes editar el periodo; el gasto liquidado aplicable se recalcula para el periodo actual, sin eliminar el historial. Los periodos diarios, semanales y mensuales se reinician, respectivamente, a medianoche UTC, el lunes a las 00:00 UTC y el primer día del mes a las 00:00 UTC. Todas las retenciones pendientes cuentan para el tope activo, incluidas las aceptadas antes del cambio de periodo, para que el trabajo en curso no eluda un límite recién reiniciado. Al liquidarse, el gasto se atribuye al momento de aceptación de su solicitud; lifetime incluye todo el historial. Las listas de modelos permitidos, el RPM y las listas de direcciones IPv4/IPv6 y bloques CIDR permitidos añaden controles independientes. Una lista vacía de modelos/IP significa que no hay restricciones; la interfaz lo indica explícitamente. Se admiten ambas familias de direcciones. Los topes de la plataforma para la cuenta siguen aplicándose. El límite restante de una clave no es el saldo de la cuenta. La aplicación correcta de las restricciones en producción requiere una configuración adecuada de confianza en los proxies. GET /v1/key devuelve id, name, mask, enabled, revoked, limit, period, expiresAt, models, rpm, ips, spent, reserved y nextReset. Los campos monetarios de la clave utilizan números en USD. limit=null significa que la clave no tiene tope de gasto; limit=0 no permite ningún gasto positivo. El saldo de la cuenta sigue siendo una restricción. No se devuelve ningún secreto. --- # Errores y límites ```json {"error":{"code":"KEY_BUDGET_EXCEEDED","message":"This key has insufficient spending allowance.","request_id":"req_demo_error"}} ``` | HTTP | Código | Recuperación | |---|---|---| | 422 | `INVALID_INPUT` | Corrige los datos de entrada antes de volver a enviar. | | 401 | `INVALID_API_KEY`, `API_KEY_INACTIVE` | Utiliza una credencial válida. | | 403 | `MODEL_NOT_ALLOWED`, `IP_NOT_ALLOWED` | Revisa las restricciones de la clave en la consola. | | 402 | `INSUFFICIENT_BALANCE`, `KEY_BUDGET_EXCEEDED` | Recarga el saldo o cambia el presupuesto autorizado. | | 404 | `MODEL_NOT_FOUND`, `REQUEST_NOT_FOUND` | Comprueba el ID y la pertenencia a la cuenta. | | 409 | `IDEMPOTENCY_CONFLICT` | Utiliza la carga original o una nueva solicitud lógica. | | 409 | Registro de solicitud en curso o `RESPONSE_CACHE_EXPIRED` | Espera o concilia; no pagues por una generación duplicada. | | 422 | `UNSUPPORTED_PARAMETER` | Corrige la elección del modelo o de los datos de entrada. | | 429 | `RATE_LIMITED` | Respeta Retry-After y reduce el tráfico. | | 502/503 | `MODEL_UNAVAILABLE` | Consulta el estado de la solicitud antes de reintentar un envío. | Los mensajes se depuran para eliminar información sensible; nunca se muestran el saldo del proveedor, los secretos del comerciante ni las trazas de pila originales del servicio de origen. `X-Request-ID` facilita la resolución de problemas. Los errores desconocidos del proveedor no deben etiquetarse falsamente como infracciones de la política de contenidos. Una vez enviadas las cabeceras SSE, un error se comunica como un evento de error SSE seguido del cierre de la conexión, no como una segunda respuesta HTTP. [Idempotencia](/docs?page=tasks&lang=es) · [Producción](/docs?page=production&lang=es) --- # Integración en producción Esta referencia acompaña a la implementación del servicio; el despliegue y la certificación de integración son pasos independientes. Antes de iniciar tráfico real, certifica cada ruta de proveedor, esquema de parámetros, cálculo de precios, uso en streaming, firma de pago y mecanismo de recuperación ante fallos en un entorno de pruebas separado. - Utiliza claves del lado del servidor, TLS, tiempos de espera explícitos, consultas periódicas acotadas e ID de solicitud. - Guarda de forma persistente los registros de idempotencia antes de enviar; nunca reintentes a ciegas un POST facturable al servicio de origen tras un fallo de transporte ambiguo. - Mantén la consistencia transaccional del libro mayor del monedero, las retenciones, los presupuestos de las claves y la liquidación de pagos en MySQL. Utiliza DynamoDB para proyecciones de eventos/consultas de solicitudes y para conservar los registros de solicitudes durante 30 días, no como otra fuente de autoridad del monedero. - Envía directamente después de guardar de forma persistente la retención y la asignación del procesamiento. Devuelve una tarea asíncrona solo después de recibir su ID del servicio de origen. El GET de tareas consulta y liquida; un conciliador periódico comprueba las tareas desatendidas y reintenta las escrituras fallidas de registros. - Los registros predeterminados contienen solo metadatos y caducan a los 30 días. No guardes prompts, contenido de chat, archivos de entrada, archivos generados, cabeceras Authorization ni secretos de pago en estos registros. - Los registros del libro mayor y de pagos tienen una política de conservación separada; eliminar metadatos de solicitudes no debe eliminar el historial financiero. Los requisitos de conservación en producción deben definirse antes del lanzamiento. - Configura alertas para la acumulación de conciliaciones pendientes, retenciones bloqueadas, intentos de liquidación duplicada, latencia de tareas, acumulación de reintentos de registros, errores de proveedores y discrepancias de pagos. Antes del lanzamiento, configura las credenciales, DNS/TLS y permisos de la carga de trabajo y valida las integraciones reales. [Tareas](/docs?page=tasks&lang=es) · [Facturación](/docs?page=billing&lang=es) MySQL es la fuente de autoridad para la coordinación de tareas, las transacciones financieras y los registros pendientes de reintento de escritura de logs. Escribe los metadatos directamente en DynamoDB con proyecciones por cuenta/clave/tiempo, particiones por fecha y LOG_SHARDS; excluye los registros caducados al consultar. Un conciliador ligero consulta las tareas desatendidas y reintenta las escrituras de registros. La web, la pasarela y el conciliador se despliegan por separado. Las respuestas correctas devuelven la URL original del resultado sin cambios. El host original controla su disponibilidad, caducidad y restricciones de origen. Guarda el resultado antes de que caduque; consultar una tarea completada no renueva la URL. Se pueden conservar metadatos y URL para recuperación, nunca los archivos generados. --- # URL de referencia y entradas temporales Cuando los metadatos del modelo admitan `image`, proporciona una URL HTTPS de imagen de referencia en un campo de nivel superior de la solicitud de imagen o vídeo. El servicio no puede acceder a las URL `blob:` del navegador. La solicitud normalizada actual no incluye varias imágenes de referencia. Para un modelo que admita imágenes de referencia, pasa una URL HTTPS pública en `image`. La API pública de inferencia acepta URL, no bytes de archivos. En Playground, solo los usuarios autenticados y verificados pueden subir imágenes o vídeos temporales a R2 privado. XXXRouter nunca descarga, almacena ni vuelve a alojar los resultados generados. Las entradas temporales admiten PNG/JPEG/WebP de hasta 10 MiB y MP4/WebM de hasta 50 MiB, con 20 intentos por minuto y cuenta. Los enlaces de lectura duran como máximo 1 hora y no se renuevan. Una regla de caducidad de un día elimina los objetos de forma asíncrona, sin garantizar su borrado físico exactamente a las 24 horas. Las URL de imágenes pueden usarse como referencia en modelos compatibles. Las URL de vídeo requieren un modelo que declare entrada de vídeo y no pueden usarse como imágenes de referencia. Mantén las URL externas accesibles durante el procesamiento. ```json {"model":"seedream-4.5","prompt":"A small blue house","image":"https://example.com/reference.png","n":1,"size":"2048x2048"} ``` Las respuestas correctas devuelven la URL original del resultado sin cambios. El host original controla su disponibilidad, caducidad y restricciones de origen. Guarda el resultado antes de que caduque; consultar una tarea completada no renueva la URL. Se pueden conservar metadatos y URL para recuperación, nunca los archivos generados. --- # Python — guía de integración Ejecuta estos ejemplos en tu servidor. Utilizan el cliente HTTP del entorno de ejecución del lenguaje, no un SDK de la plataforma. No se envía ninguna solicitud hasta que ejecutes un comando. Los modelos deben estar configurados y activados antes de usarlos. ## Preparación Entorno de ejecución: **Python 3.11+**. [Descargar el código fuente completo](/examples/python.py). Configura la URL base de tu despliegue, incluyendo `/v1` exactamente una vez, y una clave de API del lado del servidor. `https://xxxrouter.com` es el dominio oficial del servicio; para las pruebas locales, sustitúyelo por el origen de tu despliegue. El servicio aún no se ha desplegado y la certificación de integración no está completa. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Ejecutar una modalidad `chat` lee los fragmentos incrementales de SSE y el uso final; `image` recibe una imagen síncrona; `async-image` y `video` procesan HTTP 202 y consultan la tarea devuelta. Las consultas periódicas se limitan a aproximadamente cinco minutos más cualquier lectura en curso; un tiempo de espera agotado conserva la tarea para consultas posteriores. ```sh python3 python.py chat # python3 python.py image # python3 python.py async-image # python3 python.py video ``` ## Idempotencia y gestión de fallos Una nueva ejecución crea una nueva clave de idempotencia y puede generar un nuevo cargo. En una aplicación, guarda de forma persistente esa clave junto con el cuerpo exacto antes del envío. Establece `XXXROUTER_REQUEST_ID` únicamente para reanudar la misma solicitud lógica con el mismo modelo, carga, clave de API y origen. No vuelvas a ejecutar con una clave nueva después de un fallo de transporte. Estos clientes realizan un único POST de generación y nunca lo reintentan automáticamente. Registra el `X-Request-ID` de la respuesta. Los errores HTTP conservan el cuerpo de respuesta para el diagnóstico. Una tarea con `status=unknown` detiene las consultas automáticas y requiere una consulta o conciliación posterior; no es una solicitud correcta ni gratuita. Un evento de error del stream o la ausencia de `[DONE]` indica una interrupción. La ausencia de datos de uso no significa coste cero. Cerrar la conexión no cancela el trabajo aceptado. No registres claves de API ni URL firmadas de recursos. ## Imágenes de entrada y archivos de resultados Para un modelo que admita imágenes de referencia, pasa una URL HTTPS pública en `image`. La API pública de inferencia acepta URL, no bytes de archivos. En Playground, solo los usuarios autenticados y verificados pueden subir imágenes o vídeos temporales a R2 privado. XXXRouter nunca descarga, almacena ni vuelve a alojar los resultados generados. Las respuestas correctas devuelven la URL original del resultado sin cambios. El host original controla su disponibilidad, caducidad y restricciones de origen. Guarda el resultado antes de que caduque; consultar una tarea completada no renueva la URL. Se pueden conservar metadatos y URL para recuperación, nunca los archivos generados. ## Ejemplo completo El código es un ejemplo de integración, no una prueba de que un modelo real o un canal de pago esté activado. No lo incluyas en un paquete para el navegador. ```python """Python 3.11+, standard library only. No automatic generation retries.""" import json import os import sys import time import urllib.error import urllib.request import uuid BASE = os.environ['XXXROUTER_BASE_URL'].rstrip('/') # Include /v1 exactly once. KEY = os.environ['XXXROUTER_API_KEY'] MODE = sys.argv[1] if len(sys.argv) > 1 else 'chat' CASES = { 'chat': ('chat/completions', {'model': 'deepseek-v4-flash', 'messages': [{'role': 'user', 'content': 'Explain HTTP in one sentence.'}], 'max_tokens': 256, 'stream': True, 'stream_options': {'include_usage': True}}), 'image': ('images/generations', {'model': 'seedream-4.5', 'prompt': 'A ceramic cup in natural light', 'size': '2048x2048', 'n': 1}), 'async-image': ('images/generations', {'model': 'nano-banana-pro', 'prompt': 'A ceramic cup in natural light', 'size': '1024x1024', 'n': 1}), 'video': ('videos/generations', {'model': 'kling-3.0', 'prompt': 'A slow camera move across a mountain landscape', 'duration': 5, 'aspect_ratio': '16:9'}), } class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None opener = urllib.request.build_opener(NoRedirect) def request(path, payload=None, idem=None): headers = {'Authorization': 'Bearer ' + KEY} if idem: headers['Idempotency-Key'] = idem data = None if payload is not None: headers['Content-Type'] = 'application/json' data = json.dumps(payload).encode() req = urllib.request.Request(BASE + '/' + path, data=data, headers=headers) try: return opener.open(req, timeout=300 if payload is not None else 30) except urllib.error.HTTPError as error: # Read the structured failure: an uncertain request may already have a hold. print('HTTP', error.code, 'request_id=', error.headers.get('X-Request-ID'), file=sys.stderr) print(error.read().decode(), file=sys.stderr) raise def stream(response): event = [] def consume(): data = '\n'.join(event) if data == '[DONE]': return True if data: chunk = json.loads(data) if chunk.get('error'): raise RuntimeError(chunk['error']) for choice in chunk.get('choices', []): print(choice.get('delta', {}).get('content') or '', end='', flush=True) if chunk.get('usage'): print('\nusage:', chunk['usage']) return False for raw in response: line = raw.decode('utf-8').rstrip('\r\n') if line == '': if consume(): return event.clear() elif line.startswith('data:'): event.append(line[5:].lstrip(' ')) if event and consume(): return raise RuntimeError('Stream interrupted. Keep request ID and idempotency key; do not create another submission.') def finish(result): deadline = time.monotonic() + 300 while result.get('task_id') and result.get('status') not in ('succeeded', 'failed', 'unknown'): if time.monotonic() >= deadline: print(result) raise TimeoutError('Keep task_id and query later; do not resubmit.') time.sleep(2) with request('tasks/' + result['task_id']) as response: result = json.load(response) print(json.dumps(result, indent=2)) if result.get('status') == 'unknown': raise RuntimeError('Outcome unconfirmed. Retain IDs and wait for reconciliation; do not resubmit.') if result.get('status') == 'failed': raise RuntimeError('Generation failed. Inspect error and billing before a new logical request.') if __name__ == '__main__': path, payload = CASES[MODE] idem = os.environ.get('XXXROUTER_REQUEST_ID') or str(uuid.uuid4()) print('idempotency_key=', idem, file=sys.stderr) # Persist the key with this exact payload before submitting in a real application. with request(path, payload, idem) as response: print('request_id=', response.headers.get('X-Request-ID'), file=sys.stderr) if 'text/event-stream' in response.headers.get('Content-Type', ''): stream(response) else: finish(json.load(response)) ``` ## Referencias relacionadas [chat](/docs?page=chat&lang=es) · [images](/docs?page=images&lang=es) · [videos](/docs?page=videos&lang=es) · [tasks](/docs?page=tasks&lang=es) · [uploads](/docs?page=uploads&lang=es) · [errors](/docs?page=errors&lang=es) [Documentación del entorno de ejecución](https://docs.python.org/3/library/urllib.request.html) · 2026-10-26 --- # TypeScript / Node.js — guía de integración Ejecuta estos ejemplos en tu servidor. Utilizan el cliente HTTP del entorno de ejecución del lenguaje, no un SDK de la plataforma. No se envía ninguna solicitud hasta que ejecutes un comando. Los modelos deben estar configurados y activados antes de usarlos. ## Preparación Entorno de ejecución: **Node.js 22.18+**. [Descargar el código fuente completo](/examples/typescript.ts). Configura la URL base de tu despliegue, incluyendo `/v1` exactamente una vez, y una clave de API del lado del servidor. `https://xxxrouter.com` es el dominio oficial del servicio; para las pruebas locales, sustitúyelo por el origen de tu despliegue. El servicio aún no se ha desplegado y la certificación de integración no está completa. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Ejecutar una modalidad `chat` lee los fragmentos incrementales de SSE y el uso final; `image` recibe una imagen síncrona; `async-image` y `video` procesan HTTP 202 y consultan la tarea devuelta. Las consultas periódicas se limitan a aproximadamente cinco minutos más cualquier lectura en curso; un tiempo de espera agotado conserva la tarea para consultas posteriores. ```sh node --experimental-strip-types typescript.ts chat # node --experimental-strip-types typescript.ts image # node --experimental-strip-types typescript.ts async-image # node --experimental-strip-types typescript.ts video ``` ## Idempotencia y gestión de fallos Una nueva ejecución crea una nueva clave de idempotencia y puede generar un nuevo cargo. En una aplicación, guarda de forma persistente esa clave junto con el cuerpo exacto antes del envío. Establece `XXXROUTER_REQUEST_ID` únicamente para reanudar la misma solicitud lógica con el mismo modelo, carga, clave de API y origen. No vuelvas a ejecutar con una clave nueva después de un fallo de transporte. Estos clientes realizan un único POST de generación y nunca lo reintentan automáticamente. Registra el `X-Request-ID` de la respuesta. Los errores HTTP conservan el cuerpo de respuesta para el diagnóstico. Una tarea con `status=unknown` detiene las consultas automáticas y requiere una consulta o conciliación posterior; no es una solicitud correcta ni gratuita. Un evento de error del stream o la ausencia de `[DONE]` indica una interrupción. La ausencia de datos de uso no significa coste cero. Cerrar la conexión no cancela el trabajo aceptado. No registres claves de API ni URL firmadas de recursos. ## Imágenes de entrada y archivos de resultados Para un modelo que admita imágenes de referencia, pasa una URL HTTPS pública en `image`. La API pública de inferencia acepta URL, no bytes de archivos. En Playground, solo los usuarios autenticados y verificados pueden subir imágenes o vídeos temporales a R2 privado. XXXRouter nunca descarga, almacena ni vuelve a alojar los resultados generados. Las respuestas correctas devuelven la URL original del resultado sin cambios. El host original controla su disponibilidad, caducidad y restricciones de origen. Guarda el resultado antes de que caduque; consultar una tarea completada no renueva la URL. Se pueden conservar metadatos y URL para recuperación, nunca los archivos generados. ## Ejemplo completo El código es un ejemplo de integración, no una prueba de que un modelo real o un canal de pago esté activado. No lo incluyas en un paquete para el navegador. ```typescript // Node.js 22.18+; run: node --experimental-strip-types typescript.ts chat // Uses native fetch, including explicit HTTP and SSE error handling. import { randomUUID } from 'node:crypto'; type Result = { status?: string; task_id?: string; error?: unknown; [key: string]: any }; const base = process.env.XXXROUTER_BASE_URL?.replace(/\/$/, ''); // Include /v1. const key = process.env.XXXROUTER_API_KEY; if (!base || !key) throw new Error('Set XXXROUTER_BASE_URL and XXXROUTER_API_KEY'); const cases: Record]> = { chat: ['chat/completions', { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'Explain HTTP in one sentence.' }], max_tokens: 256, stream: true, stream_options: { include_usage: true } }], image: ['images/generations', { model: 'seedream-4.5', prompt: 'A ceramic cup in natural light', size: '2048x2048', n: 1 }], 'async-image': ['images/generations', { model: 'nano-banana-pro', prompt: 'A ceramic cup in natural light', size: '1024x1024', n: 1 }], video: ['videos/generations', { model: 'kling-3.0', prompt: 'A slow camera move across a mountain landscape', duration: 5, aspect_ratio: '16:9' }], }; async function request(path: string, payload?: unknown, idem?: string): Promise { const headers: Record = { Authorization: `Bearer ${key}` }; if (idem) headers['Idempotency-Key'] = idem; if (payload) headers['Content-Type'] = 'application/json'; const response = await fetch(`${base}/${path}`, { method: payload ? 'POST' : 'GET', headers, redirect: 'error', body: payload ? JSON.stringify(payload) : undefined, signal: AbortSignal.timeout(payload ? 300_000 : 30_000), }); console.error('request_id=', response.headers.get('X-Request-ID')); if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); return response; } async function stream(response: Response): Promise { if (!response.body) throw new Error('Empty response body'); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ''; let data: string[] = []; const consume = (): boolean => { const value = data.join('\n'); data = []; if (value === '[DONE]') return true; if (!value) return false; const chunk = JSON.parse(value); if (chunk.error) throw new Error(JSON.stringify(chunk.error)); for (const choice of chunk.choices ?? []) process.stdout.write(choice.delta?.content ?? ''); if (chunk.usage) console.log('\nusage:', chunk.usage); return false; }; try { while (true) { const { value, done } = await reader.read(); buffer += value ?? ''; let end: number; while ((end = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, end).replace(/\r$/, ''); buffer = buffer.slice(end + 1); if (!line && consume()) return; if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, '')); } if (done) break; } if (buffer.startsWith('data:')) data.push(buffer.slice(5).trimStart()); if (consume()) return; throw new Error('Stream interrupted. Keep request ID and idempotency key; do not resubmit.'); } finally { await reader.cancel(); reader.releaseLock(); } } async function finish(result: Result): Promise { const deadline = Date.now() + 300_000; while (result.task_id && !['succeeded', 'failed', 'unknown'].includes(result.status ?? '')) { if (Date.now() >= deadline) throw new Error(`Query task ${result.task_id} later; do not resubmit`); await new Promise(resolve => setTimeout(resolve, 2000)); result = await (await request(`tasks/${encodeURIComponent(result.task_id)}`)).json(); } console.log(result); if (result.status === 'unknown') throw new Error('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (result.status === 'failed') throw new Error('Generation failed; inspect error and billing.'); } const [path, payload] = cases[process.argv[2] ?? 'chat']; const idem = process.env.XXXROUTER_REQUEST_ID || randomUUID(); console.error('idempotency_key=', idem); // Persist alongside the exact payload before submitting. const response = await request(path, payload, idem); if (response.headers.get('content-type')?.includes('text/event-stream')) await stream(response); else await finish(await response.json()); ``` ## Referencias relacionadas [chat](/docs?page=chat&lang=es) · [images](/docs?page=images&lang=es) · [videos](/docs?page=videos&lang=es) · [tasks](/docs?page=tasks&lang=es) · [uploads](/docs?page=uploads&lang=es) · [errors](/docs?page=errors&lang=es) [Documentación del entorno de ejecución](https://nodejs.org/api/globals.html#fetch) · 2026-10-26 --- # Go — guía de integración Ejecuta estos ejemplos en tu servidor. Utilizan el cliente HTTP del entorno de ejecución del lenguaje, no un SDK de la plataforma. No se envía ninguna solicitud hasta que ejecutes un comando. Los modelos deben estar configurados y activados antes de usarlos. ## Preparación Entorno de ejecución: **Go 1.22+**. [Descargar el código fuente completo](/examples/go.go). Configura la URL base de tu despliegue, incluyendo `/v1` exactamente una vez, y una clave de API del lado del servidor. `https://xxxrouter.com` es el dominio oficial del servicio; para las pruebas locales, sustitúyelo por el origen de tu despliegue. El servicio aún no se ha desplegado y la certificación de integración no está completa. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Ejecutar una modalidad `chat` lee los fragmentos incrementales de SSE y el uso final; `image` recibe una imagen síncrona; `async-image` y `video` procesan HTTP 202 y consultan la tarea devuelta. Las consultas periódicas se limitan a aproximadamente cinco minutos más cualquier lectura en curso; un tiempo de espera agotado conserva la tarea para consultas posteriores. ```sh go run go.go chat # go run go.go image # go run go.go async-image # go run go.go video ``` ## Idempotencia y gestión de fallos Una nueva ejecución crea una nueva clave de idempotencia y puede generar un nuevo cargo. En una aplicación, guarda de forma persistente esa clave junto con el cuerpo exacto antes del envío. Establece `XXXROUTER_REQUEST_ID` únicamente para reanudar la misma solicitud lógica con el mismo modelo, carga, clave de API y origen. No vuelvas a ejecutar con una clave nueva después de un fallo de transporte. Estos clientes realizan un único POST de generación y nunca lo reintentan automáticamente. Registra el `X-Request-ID` de la respuesta. Los errores HTTP conservan el cuerpo de respuesta para el diagnóstico. Una tarea con `status=unknown` detiene las consultas automáticas y requiere una consulta o conciliación posterior; no es una solicitud correcta ni gratuita. Un evento de error del stream o la ausencia de `[DONE]` indica una interrupción. La ausencia de datos de uso no significa coste cero. Cerrar la conexión no cancela el trabajo aceptado. No registres claves de API ni URL firmadas de recursos. ## Imágenes de entrada y archivos de resultados Para un modelo que admita imágenes de referencia, pasa una URL HTTPS pública en `image`. La API pública de inferencia acepta URL, no bytes de archivos. En Playground, solo los usuarios autenticados y verificados pueden subir imágenes o vídeos temporales a R2 privado. XXXRouter nunca descarga, almacena ni vuelve a alojar los resultados generados. Las respuestas correctas devuelven la URL original del resultado sin cambios. El host original controla su disponibilidad, caducidad y restricciones de origen. Guarda el resultado antes de que caduque; consultar una tarea completada no renueva la URL. Se pueden conservar metadatos y URL para recuperación, nunca los archivos generados. ## Ejemplo completo El código es un ejemplo de integración, no una prueba de que un modelo real o un canal de pago esté activado. No lo incluyas en un paquete para el navegador. ```go // Go 1.22+, standard library only. Run: go run go.go chat package main import ( "bufio" "bytes" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) type Object = map[string]any var base = strings.TrimRight(os.Getenv("XXXROUTER_BASE_URL"), "/") // Include /v1. var key = os.Getenv("XXXROUTER_API_KEY") var client = &http.Client{Timeout: 300 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} func request(path string, payload Object, idem string) (*http.Response, error) { var body io.Reader method := "GET" if payload != nil { raw, err := json.Marshal(payload) if err != nil { return nil, err } body = bytes.NewReader(raw) method = "POST" } req, err := http.NewRequest(method, base+"/"+path, body) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) if payload != nil { req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", idem) } response, err := client.Do(req) if err != nil { return nil, err } fmt.Fprintln(os.Stderr, "request_id=", response.Header.Get("X-Request-ID")) if response.StatusCode < 200 || response.StatusCode >= 300 { defer response.Body.Close() raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20)) return nil, fmt.Errorf("HTTP %d: %s", response.StatusCode, raw) } return response, nil } func decode(response *http.Response) (Object, error) { defer response.Body.Close() var result Object err := json.NewDecoder(response.Body).Decode(&result) return result, err } func stream(response *http.Response) error { defer response.Body.Close() scanner := bufio.NewScanner(response.Body) scanner.Buffer(make([]byte, 4096), 2<<20) var parts []string consume := func() (bool, error) { data := strings.Join(parts, "\n") parts = nil if data == "[DONE]" { return true, nil } if data == "" { return false, nil } var chunk struct { Error json.RawMessage `json:"error"` Usage json.RawMessage `json:"usage"` Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` } if err := json.Unmarshal([]byte(data), &chunk); err != nil { return false, err } if len(chunk.Error) > 0 && string(chunk.Error) != "null" { return false, fmt.Errorf("stream error: %s", chunk.Error) } for _, choice := range chunk.Choices { fmt.Print(choice.Delta.Content) } if len(chunk.Usage) > 0 && string(chunk.Usage) != "null" { fmt.Printf("\nusage: %s\n", chunk.Usage) } return false, nil } for scanner.Scan() { line := scanner.Text() if line == "" { done, err := consume() if err != nil || done { return err } } if strings.HasPrefix(line, "data:") { parts = append(parts, strings.TrimPrefix(line[5:], " ")) } } if err := scanner.Err(); err != nil { return err } done, err := consume() if err != nil || done { return err } return fmt.Errorf("stream interrupted; keep request ID and idempotency key, do not resubmit") } func finish(result Object) error { deadline := time.Now().Add(5 * time.Minute) for { task, hasTask := result["task_id"].(string) status, _ := result["status"].(string) if !hasTask || status == "succeeded" || status == "failed" || status == "unknown" { break } if time.Now().After(deadline) { return fmt.Errorf("query task %s later; do not resubmit", task) } time.Sleep(2 * time.Second) response, err := request("tasks/"+task, nil, "") if err != nil { return err } result, err = decode(response) if err != nil { return err } } raw, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(raw)) if result["status"] == "unknown" { return fmt.Errorf("outcome unconfirmed; retain IDs and wait for reconciliation") } if result["status"] == "failed" { return fmt.Errorf("generation failed; inspect error and billing") } return nil } func run() error { if base == "" || key == "" { return fmt.Errorf("set XXXROUTER_BASE_URL and XXXROUTER_API_KEY") } mode := "chat" if len(os.Args) > 1 { mode = os.Args[1] } path := "images/generations" var payload Object switch mode { case "chat": path = "chat/completions" payload = Object{"model": "deepseek-v4-flash", "messages": []Object{{"role": "user", "content": "Explain HTTP in one sentence."}}, "max_tokens": 256, "stream": true, "stream_options": Object{"include_usage": true}} case "image": payload = Object{"model": "seedream-4.5", "prompt": "A ceramic cup in natural light", "size": "2048x2048", "n": 1} case "async-image": payload = Object{"model": "nano-banana-pro", "prompt": "A ceramic cup in natural light", "size": "1024x1024", "n": 1} case "video": path = "videos/generations" payload = Object{"model": "kling-3.0", "prompt": "A slow camera move across a mountain landscape", "duration": 5, "aspect_ratio": "16:9"} default: return fmt.Errorf("choose chat, image, async-image or video") } idem := os.Getenv("XXXROUTER_REQUEST_ID") if idem == "" { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return err } idem = hex.EncodeToString(b) } fmt.Fprintln(os.Stderr, "idempotency_key=", idem) // Persist with the exact payload before submission. response, err := request(path, payload, idem) if err != nil { return err } if strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { return stream(response) } result, err := decode(response) if err != nil { return err } return finish(result) } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } ``` ## Referencias relacionadas [chat](/docs?page=chat&lang=es) · [images](/docs?page=images&lang=es) · [videos](/docs?page=videos&lang=es) · [tasks](/docs?page=tasks&lang=es) · [uploads](/docs?page=uploads&lang=es) · [errors](/docs?page=errors&lang=es) [Documentación del entorno de ejecución](https://pkg.go.dev/net/http) · 2026-10-26 --- # PHP — guía de integración Ejecuta estos ejemplos en tu servidor. Utilizan el cliente HTTP del entorno de ejecución del lenguaje, no un SDK de la plataforma. No se envía ninguna solicitud hasta que ejecutes un comando. Los modelos deben estar configurados y activados antes de usarlos. ## Preparación Entorno de ejecución: **PHP 8.1+ + ext-curl**. [Descargar el código fuente completo](/examples/php.php). Configura la URL base de tu despliegue, incluyendo `/v1` exactamente una vez, y una clave de API del lado del servidor. `https://xxxrouter.com` es el dominio oficial del servicio; para las pruebas locales, sustitúyelo por el origen de tu despliegue. El servicio aún no se ha desplegado y la certificación de integración no está completa. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Ejecutar una modalidad `chat` lee los fragmentos incrementales de SSE y el uso final; `image` recibe una imagen síncrona; `async-image` y `video` procesan HTTP 202 y consultan la tarea devuelta. Las consultas periódicas se limitan a aproximadamente cinco minutos más cualquier lectura en curso; un tiempo de espera agotado conserva la tarea para consultas posteriores. ```sh php php.php chat # php php.php image # php php.php async-image # php php.php video ``` ## Idempotencia y gestión de fallos Una nueva ejecución crea una nueva clave de idempotencia y puede generar un nuevo cargo. En una aplicación, guarda de forma persistente esa clave junto con el cuerpo exacto antes del envío. Establece `XXXROUTER_REQUEST_ID` únicamente para reanudar la misma solicitud lógica con el mismo modelo, carga, clave de API y origen. No vuelvas a ejecutar con una clave nueva después de un fallo de transporte. Estos clientes realizan un único POST de generación y nunca lo reintentan automáticamente. Registra el `X-Request-ID` de la respuesta. Los errores HTTP conservan el cuerpo de respuesta para el diagnóstico. Una tarea con `status=unknown` detiene las consultas automáticas y requiere una consulta o conciliación posterior; no es una solicitud correcta ni gratuita. Un evento de error del stream o la ausencia de `[DONE]` indica una interrupción. La ausencia de datos de uso no significa coste cero. Cerrar la conexión no cancela el trabajo aceptado. No registres claves de API ni URL firmadas de recursos. ## Imágenes de entrada y archivos de resultados Para un modelo que admita imágenes de referencia, pasa una URL HTTPS pública en `image`. La API pública de inferencia acepta URL, no bytes de archivos. En Playground, solo los usuarios autenticados y verificados pueden subir imágenes o vídeos temporales a R2 privado. XXXRouter nunca descarga, almacena ni vuelve a alojar los resultados generados. Las respuestas correctas devuelven la URL original del resultado sin cambios. El host original controla su disponibilidad, caducidad y restricciones de origen. Guarda el resultado antes de que caduque; consultar una tarea completada no renueva la URL. Se pueden conservar metadatos y URL para recuperación, nunca los archivos generados. ## Ejemplo completo El código es un ejemplo de integración, no una prueba de que un modelo real o un canal de pago esté activado. No lo incluyas en un paquete para el navegador. ```php getMessage(); } }; $curl = curl_init($base . '/' . $path); $options = [ CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $payload !== null ? 300 : 30, CURLOPT_HEADERFUNCTION => function ($handle, string $line) use (&$contentType, &$requestId): int { if (stripos($line, 'Content-Type:') === 0) $contentType = trim(substr($line, 13)); if (stripos($line, 'X-Request-ID:') === 0) $requestId = trim(substr($line, 13)); return strlen($line); }, CURLOPT_WRITEFUNCTION => function ($handle, string $bytes) use (&$contentType, &$raw, &$buffer, &$data, &$done, &$streamError, $consume): int { if (!str_contains($contentType, 'text/event-stream')) { $raw .= $bytes; return strlen($bytes); } $buffer .= $bytes; while (($end = strpos($buffer, "\n")) !== false) { $line = rtrim(substr($buffer, 0, $end), "\r"); $buffer = substr($buffer, $end + 1); if ($line === '') $consume(); elseif (str_starts_with($line, 'data:')) $data[] = ltrim(substr($line, 5), ' '); } return strlen($bytes); }, ]; if ($payload !== null) { $headers[] = 'Content-Type: application/json'; $headers[] = 'Idempotency-Key: ' . $idem; $options[CURLOPT_POST] = true; $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_THROW_ON_ERROR); } $options[CURLOPT_HTTPHEADER] = $headers; curl_setopt_array($curl, $options); try { $ok = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); fwrite(STDERR, 'request_id=' . $requestId . "\n"); if ($ok === false) throw new RuntimeException('Transport error; retain IDs, do not resubmit: ' . curl_error($curl)); if ($status < 200 || $status >= 300) throw new RuntimeException('HTTP ' . $status . ': ' . $raw); if (str_contains($contentType, 'text/event-stream')) { if (str_starts_with($buffer, 'data:')) $data[] = ltrim(substr($buffer, 5), ' '); $consume(); if ($streamError !== null) throw new RuntimeException($streamError); if (!$done) throw new RuntimeException('Stream interrupted; retain request ID and idempotency key.'); return ['stream_complete' => true]; } return json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } finally { curl_close($curl); } } $cases = [ 'chat' => ['chat/completions', ['model'=>'deepseek-v4-flash','messages'=>[['role'=>'user','content'=>'Explain HTTP in one sentence.']],'max_tokens'=>256,'stream'=>true,'stream_options'=>['include_usage'=>true]]], 'image' => ['images/generations', ['model'=>'seedream-4.5','prompt'=>'A ceramic cup in natural light','size'=>'2048x2048','n'=>1]], 'async-image' => ['images/generations', ['model'=>'nano-banana-pro','prompt'=>'A ceramic cup in natural light','size'=>'1024x1024','n'=>1]], 'video' => ['videos/generations', ['model'=>'kling-3.0','prompt'=>'A slow camera move across a mountain landscape','duration'=>5,'aspect_ratio'=>'16:9']], ]; $mode = $argv[1] ?? 'chat'; if (!isset($cases[$mode])) throw new InvalidArgumentException('Choose chat, image, async-image or video'); [$path, $payload] = $cases[$mode]; $idem = getenv('XXXROUTER_REQUEST_ID') ?: bin2hex(random_bytes(16)); fwrite(STDERR, 'idempotency_key=' . $idem . "\n"); // Persist alongside exact payload before submission. $result = request($path, $payload, $idem); $deadline = microtime(true) + 300; while (isset($result['task_id']) && !in_array($result['status'], ['succeeded', 'failed', 'unknown'], true)) { if (microtime(true) >= $deadline) throw new RuntimeException('Query task ' . $result['task_id'] . ' later; do not resubmit.'); sleep(2); $result = request('tasks/' . rawurlencode($result['task_id'])); } echo json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . "\n"; if (($result['status'] ?? '') === 'unknown') throw new RuntimeException('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (($result['status'] ?? '') === 'failed') throw new RuntimeException('Generation failed; inspect error and billing.'); ``` ## Referencias relacionadas [chat](/docs?page=chat&lang=es) · [images](/docs?page=images&lang=es) · [videos](/docs?page=videos&lang=es) · [tasks](/docs?page=tasks&lang=es) · [uploads](/docs?page=uploads&lang=es) · [errors](/docs?page=errors&lang=es) [Documentación del entorno de ejecución](https://www.php.net/manual/en/function.curl-exec.php) · 2026-10-26 --- # Historial de cambios ## 0.1.0 — 2026-10-26 — Borrador de implementación - Se añadieron el saldo de cuenta en USD y los controles de gasto por clave. - Se añadieron la referencia para desarrolladores en en / zh-CN y el borrador de OpenAPI legible por máquinas. - Se distinguieron los resultados directos de imágenes síncronas de las tareas asíncronas de imágenes/vídeos. - Se definió el chat de texto de estilo OpenAI con SSE opcional. - Se añadieron el crédito de prueba de $0.20 con caducidad de 7 días y las páginas de pago alojadas de Payblis / NOWPayments. - Se estableció la política predeterminada de registros de solicitudes en solo metadatos, durante 30 días. Esta referencia describe los endpoints implementados. El despliegue, la certificación de rutas reales y la aceptación de los canales de pago siguen siendo requisitos de lanzamiento separados. [Inicio rápido](/docs?page=quickstart&lang=es) --- # Быстрый старт > Справочник по реализации · 2026-10-26 · Настройте развёртывание перед отправкой трафика. Приложение использует серверные аутентификацию, баланс и обработку запросов. `https://xxxrouter.com` — официальный домен сервиса; для локального тестирования замените его на origin своего развёртывания. Сервис пока не развёрнут, а проверка и сертификация интеграции не завершены. Через шлюз поддерживается `/v1` на том же origin. Изначально модели отключены; перед включением операторы должны настроить учётные данные, проверить маршруты и задать цены. Этот справочник не подтверждает развёртывание или завершение интеграционных испытаний. 1. Войдите в консоль и проверьте баланс в USD. 2. Создайте API-ключ. Скопируйте его при однократном показе и сохраните в менеджере секретов вашего сервера. 3. Проверьте режим ответа модели и применимую цену. 4. Вызовите API из своего бэкенда. Отслеживайте возвращённый ID запроса в разделе использования и журналов. ```sh export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' curl https://xxxrouter.com/v1/models \ -H "Authorization: Bearer $XXXROUTER_API_KEY" ``` ```sh curl https://xxxrouter.com/v1/chat/completions \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-chat-request-001' \ -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Hello"}],"max_tokens":256,"stream":false}' ``` Чат следует контракту в стиле OpenAI. API изображений поддерживают как непосредственные результаты, так и ответы с задачами; они не являются универсальной заменой OpenAI Images без изменений в клиенте. Playground в консоли использует те же цены и правила учёта, что и внешние вызовы API, работает через сессию вошедшего пользователя и учитывается отдельно от API-ключей. Далее: [аутентификация](/docs?page=auth&lang=ru), [возможности моделей](/docs?page=models&lang=ru), [изображения](/docs?page=images&lang=ru), [ошибки](/docs?page=errors&lang=ru). Машиночитаемые интерфейсы: [OpenAPI](/openapi.json), [llms.txt](/llms.txt), [полный текст](/llms-full.txt). --- # Аутентификация Передавайте `Authorization: Bearer ` в запросах генерации, получения задач и сведений о ключе. GET-методы каталога моделей доступны анонимно и показывают публичные цены; действительный ключ, переданный по желанию, позволяет получить цены для аккаунта. Никогда не помещайте ключи в строки запроса, публичные репозитории, клиентские сборки, общедоступные снимки экрана или хранилище браузера. Создайте настоящий ключ в консоли, войдя в аккаунт с подтверждённой электронной почтой. Действующий ключ показывается целиком только один раз — при создании. При последующих просмотрах доступны его префикс, суффикс и неизменяемый ID. Сервер хранит необратимый хеш с секретным ключом, а не открытый текст, который можно восстановить. Для управления ключами нужна сессия консоли; ключ для вызова моделей не позволяет создавать другие ключи или изменять платёжные настройки аккаунта. Чтобы заменить ключ, создайте новый, внедрите его, убедитесь, что запросы проходят, затем отзовите исходный. Приостановку можно отменить; отзыв окончателен. Срок действия проверяется по UTC при каждом новом запросе. Уже принятые задачи сохраняют начисления и историю после приостановки, истечения срока или отзыва ключа. Для их просмотра используйте другой авторизованный ключ того же аккаунта или консоль. `GET /v1/key` возвращает метаданные текущего ключа, включая действующие ограничения и оставшийся лимит расходов. Секретное значение не возвращается никогда. Ошибки не раскрывают, принадлежит ли подобранный ключ другому аккаунту. [Управление ключами](/docs?page=keys&lang=ru) · [Ошибки](/docs?page=errors&lang=ru) --- # Модели и цены `GET /v1/models` перечисляет модели, доступные вашему аккаунту. `GET /v1/models/{id}` возвращает возможности, параметры, режим ответа, доступность и применимую цену. Публичные ID моделей — это имена платформы без префиксов маршрутизации; например, `/v1/models/seedream-4.5`. Метаданные маршрутизации и инфраструктуры являются внутренними и не возвращаются публичным API моделей. | Демонстрационный ID модели | Результат | Режим ответа | |---|---|---| | `seedream-4.5` | Изображение | `sync` | | `nano-banana-pro` | Изображение | `async` | | `flux-2-pro` | Изображение | `async` | | `kling-3.0` | Видео | `async` | | `deepseek-v4-flash` | Текст | Режим `stream` в каталоге; JSON или SSE выбирается через `stream` | Это публичные ID моделей платформы, а не гарантии того, что подключение настроено, проверено и доступно в рабочей среде. Режим ответа модели явно указан в метаданных каталога; его никогда не определяют по подстроке URL или ключевому слову `async` языка программирования. Возможности модели проверяются до резервирования средств и отправки запроса провайдеру. Цены указаны в USD. Тарифы на изображения зависят от поддерживаемых параметров, тарифы на видео в этом выпуске — от длительности, а тарифы LLM отдельно учитывают входные и выходные токены. При принятии запроса сохраняется версия применимой цены; последующие изменения каталога на неё не влияют. Оценка стоимости не является окончательным списанием. [Протокол изображений](/docs?page=images&lang=ru) · [Тарификация](/docs?page=billing&lang=ru) ## Приоритет применимых цен Применяется ровно одно подходящее правило: сначала индивидуальная цена пользователя для модели, затем цена группы для модели, затем скидка группы по умолчанию, затем публичная цена. Индивидуальные правила могут задавать фиксированную цену или скидку в допустимом для модели диапазоне. Правила не суммируются. Цена ниже себестоимости требует явного подтверждения оператора; при неизвестной стоимости поставщика нельзя показывать вымышленную маржу. Выбранное правило, единицы тарификации и версия фиксируются при принятии запроса. Список `parameters` модели объявляет нормализованные поля верхнего уровня: изображения используют `size`, `n`, `image`; видео — `duration`, `aspect_ratio`, `image`; чат — `temperature`, `max_tokens`, `stream`, `stream_options`. Это не словарь для произвольной сквозной передачи параметров. В примерах Seedream 4.5 используется `2048x2048`; ограничения параметров в рабочей среде необходимо проверить для каждой модели. Варианты в демонстрационном интерфейсе не гарантируют доступность соответствующих возможностей в рабочей среде. JSON модели использует name, modality (llm/image/video), mode/response_mode (stream/sync/async), parameters, sizes, supportsImage и enabled. pricing — объект с currency, price и допускающим null полем outputPrice (строки USD с шестью знаками после десятичной точки); price/outputPrice — числовые поля USD. Отключённую модель вызвать нельзя. Начальные цены требуют проверки оператором; неизвестная себестоимость никогда не представляется в виде маржи. --- # Генерация ответов в чате `POST /v1/chat/completions` принимает `model`, `messages`, `max_tokens`, `temperature`, `stream` и необязательный `stream_options: {"include_usage": true}`. Контракт v0.1 охватывает текстовые сообщения с ролями `system`, `user` и `assistant`. Вызовы инструментов, мультимодальный чат, embeddings и Responses API не входят в контракт этого выпуска. ```json {"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Explain HTTP in one sentence."}],"max_tokens":256,"temperature":0.7,"stream":true,"stream_options":{"include_usage":true}} ``` Непотоковый результат содержит `id`, `object: "chat.completion"`, `model`, `choices` и `usage`. Потоковый ответ имеет тип `text/event-stream`; каждое событие data содержит фрагмент ответа с `choices[].delta`. При `stream_options.include_usage=true` завершающее событие использования от вышестоящего сервиса, если оно предоставлено, предшествует `data: [DONE]`. Сохраняйте `X-Request-ID` из заголовков ответа. Не разбирайте SSE как один JSON-документ. Шлюз не должен добавлять системную инструкцию, требующую исключительно JSON, незаметно менять модель или повторять оплачиваемую генерацию ради исправления результата. Разрыв соединения или нажатие «Стоп» не отменяет уже сгенерированные токены. Окончательное списание основано на зафиксированном использовании; при отсутствии достоверных данных оно остаётся на сверке, а не молча становится нулевым. Каждый маршрут необходимо проверить на совместимость перед включением. [Тарификация](/docs?page=billing&lang=ru) · [Промышленная эксплуатация](/docs?page=production&lang=ru) Для всех POST-запросов генерации обязателен Idempotency-Key (1–128 печатных символов ASCII). Для нового логического запроса используйте новое значение, а прежнее — только для точного повтора. max_tokens допускает значения 1–4096; temperature — 0–2. Повтор потокового запроса не запускает заново и не воспроизводит оплаченный поток вышестоящего сервиса. --- # Генерация изображений `POST /v1/images/generations` предусматривает два явно объявленных режима ответа, выбираемых по каталогу моделей. Всегда проверяйте метаданные модели и обрабатывайте документированный ответ. Оба режима принимают `model`, `prompt`, необязательный URL референсного изображения в поле верхнего уровня `image`, `size` и `n`. Это нормализованные поля платформы; адаптеры провайдеров преобразуют их в проверенную схему модели. ```sh curl https://xxxrouter.com/v1/images/generations \ -H "Authorization: Bearer $XXXROUTER_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: sample-image-request-001' \ -d '{"model":"seedream-4.5","prompt":"A ceramic cup in natural light","size":"2048x2048","n":1}' ``` ## Синхронная модель: HTTP 200 ```json {"request_id":"req_demo_sync","model":"seedream-4.5","response_mode":"sync","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/cup.png"}],"billing":{"status":"settled","currency":"USD","amount":"0.030000"}} ``` Изображение приходит в исходном HTTP-ответе. ID задачи провайдера и последующий опрос задачи не требуются. Указанная выше сумма приведена для примера и не является ценовым предложением. Медленное синхронное выполнение остаётся синхронным; тайм-аут клиента не доказывает сбой и не разрешает повторную отправку. ## Асинхронная модель: HTTP 202 Выберите `nano-banana-pro` или `flux-2-pro`, чтобы получить: ```json {"request_id":"req_demo_async","task_id":"task_demo_01","model":"nano-banana-pro","response_mode":"async","status":"running"} ``` Опрашивайте [метод получения задачи](/docs?page=tasks&lang=ru) до конечного состояния. Не считайте `202` готовым результатом. Общий маршрут генерации изображений не означает, что все модели совместимы с OpenAI Images. Настроенные маршруты отправляют реальные запросы. При отсутствии учётных данных имитация оплаченного результата не выполняется. [Референсные изображения](/docs?page=uploads&lang=ru) · [Ошибки](/docs?page=errors&lang=ru) Текущий сервис поддерживает только n=1. image разрешён лишь в том случае, если присутствует среди параметров модели. Успешный ответ возвращает исходный URL результата без изменений. Доступность, срок действия и междоменные ограничения определяет исходный хост. Сохраните результат до истечения срока: повторный запрос завершённой задачи не продлевает ссылку. Метаданные и URL могут сохраняться для восстановления, но не сгенерированные файлы. --- # Генерация видео `POST /v1/videos/generations` возвращает HTTP 202 и задачу. Публичный ID модели — `kling-3.0`; доступность в рабочей среде требует настроенного и проверенного маршрута. ```json {"model":"kling-3.0","prompt":"A slow camera movement around a ceramic cup","duration":5,"aspect_ratio":"16:9"} ``` Передавайте `Idempotency-Key` при каждой логической отправке. Сохраняйте `task_id` и опрашивайте `/v1/tasks/{task_id}`. Успешные результаты содержат URL видео, тип медиафайла и окончательные сведения о списании. Не обещайте отмену или процент выполнения у поставщика, если провайдер действительно не поддерживает эти возможности. Без достоверных данных точный прогресс не указывается. Указанная длительность — входной параметр расчёта цены, а не гарантия точной длительности полученного видео. До запуска для каждого маршрута необходимо документировать, как тарифицируются длительность результата и сочетания параметров. Автоматическое переключение на другую модель не предполагается. [Задачи](/docs?page=tasks&lang=ru) · [Тарификация](/docs?page=billing&lang=ru) Нормализованные параметры верхнего уровня: `duration`, `aspect_ratio` и необязательный `image`. Текущая схема принимает 5/10 секунд и 16:9, 9:16 или 1:1; перед вызовом проверьте доступность модели. В этом черновике запроса нет поля `resolution` или вложенного поля `parameters`. Успешный ответ возвращает исходный URL результата без изменений. Доступность, срок действия и междоменные ограничения определяет исходный хост. Сохраните результат до истечения срока: повторный запрос завершённой задачи не продлевает ссылку. Метаданные и URL могут сохраняться для восстановления, но не сгенерированные файлы. --- # Задачи и идемпотентность `GET /v1/tasks/{id}` получает сведения о принятой задаче генерации изображения или видео. Аутентификация и принадлежность аккаунту проверяются при каждом чтении. Другой действительный ключ того же аккаунта может просматривать задачу; исходный ID ключа остаётся неизменяемой привязкой использования. Состояния: `submitting → running → succeeded | failed`. При неопределённом исходе запрос получает `status: "unknown"` и `billing.status: "reconciling"`; автоматически отправлять его заново нельзя. Успешный результат задачи и состояние оплаты независимы: результат может быть доступен, пока окончательное списание находится на сверке. ```json {"task_id":"task_demo_01","request_id":"req_demo_async","model":"nano-banana-pro","response_mode":"async","status":"succeeded","data":[{"url":"https://assets.xxxrouter.example/demo/image.png","media_type":"image"}],"billing":{"status":"settled","currency":"USD","amount":"0.150000"}} ``` Используйте ограниченный интервал опроса: начните с 2 секунд и постепенно увеличивайте до 10 секунд; соблюдайте `Retry-After`. Повторы GET не создают новую работу. Потеря ответа POST — иной случай: используйте тот же ключ идемпотентности и идентичное тело запроса. Одинаковые аккаунт + метод API + ключ + отпечаток запроса возвращают исходную запись; другое тело возвращает `409 IDEMPOTENCY_CONFLICT`. Записи идемпотентности постоянно сохраняются в MySQL. Не считайте наступление нового дня основанием для безопасного повторного использования старого ключа или повторной отправки запроса с неопределённым исходом. Для синхронных изображений повтор возвращает сохранённый непосредственный результат, если он доступен. Выполняющиеся или неопределённые синхронные запросы возвращают HTTP `409` с текущей записью запроса; повторной отправки вышестоящему сервису не происходит. ID синхронных запросов нельзя опрашивать через `/tasks`, как если бы они были ID задач. [Ошибки](/docs?page=errors&lang=ru) · [Промышленная эксплуатация](/docs?page=production&lang=ru) Успешный ответ возвращает исходный URL результата без изменений. Доступность, срок действия и междоменные ограничения определяет исходный хост. Сохраните результат до истечения срока: повторный запрос завершённой задачи не продлевает ссылку. Метаданные и URL могут сохраняться для восстановления, но не сгенерированные файлы. --- # Баланс и тарификация У аккаунта один предоплаченный баланс в USD. Лимиты ключей ограничивают расходы, но не создают отдельные кошельки. Доступные, зарезервированные и окончательно списанные суммы отображаются раздельно. Денежные значения следует хранить в целых микродолларах или точном десятичном формате, а не в двоичном формате с плавающей точкой. Предусмотренный пробный бонус составляет $0.20 и действует 7 дней. Он учитывается отдельно от оплаченных средств и не является наличными деньгами. Сервер проверяет право на получение и обеспечивает однократное начисление. Подтвердите электронную почту и пройдите проверку; срок действия бонуса применяется независимо от оплаченных средств. Прежде чем принять запрос в работу, сервис атомарно резервирует достаточную сумму на балансе аккаунта и в лимите ключа. После завершения он списывает фактическую стоимость и освобождает разницу. При сбое без оплачиваемого использования резерв освобождается. Неопределённый результат у поставщика остаётся на сверке, не вызывая повторного списания. Тайм-аут или остановка потока не означают отсутствия использования. В текущий объём входят только платёжные провайдеры Payblis и NOWPayments. Настроенные каналы создают заказы с переходом на размещённую у провайдера платёжную страницу; при отсутствии учётных данных канал недоступен. Перенаправление на страницу успешной оплаты никогда не зачисляет средства. Проверенные обратные уведомления и независимая сверка должны подтвердить валюту, сумму, идентификатор провайдера и принадлежность заказа, прежде чем будет добавлена уникальная запись в финансовый журнал. Реализованные состояния заказа: `creating`, `pending`, `confirming`, `paid`, `underpaid`, `expired` и `failed`. Частичная оплата сохраняет состояние `underpaid`, пока не подтверждена полная сумма. API возвратов и автоматическая обработка возвратов в этом выпуске не реализованы. Любой будущий возврат должен добавлять компенсирующие проводки, а не переписывать исторические операции. [Управление ключами](/docs?page=keys&lang=ru) · [Промышленная эксплуатация](/docs?page=production&lang=ru) ## Условия получения бонуса и порядок расходования средств Перед получением бонуса $0.20 подтвердите почту аккаунта и пройдите проверку. Получение идемпотентно и ограничено по частоте. Бонус истекает ровно через 7 дней после начисления; действующие бонусные средства расходуются раньше оплаченных. Для пробных запросов действуют те же применимые цены и лимиты ключа и аккаунта. Резерв сохраняет привязку к источнику средств. Если резерв освобождается после истечения бонуса, его бонусная часть возвращается в просроченный бонусный баланс, а не в доступные оплаченные средства. Истечение срока никогда не удаляет оплаченные средства. История финансового журнала хранится постоянно и не зависит от журналов запросов со сроком хранения 30 дней. --- # Управление API-ключами Управляйте ключами в консоли; `GET /v1/key` — это API только для чтения сведений о текущем ключе. У каждого ключа есть неизменяемый ID, отображаемое имя, замаскированное секретное значение, состояние, время создания, необязательный срок действия, лимит расходов и привязка использования. Для выполнения запроса должны одновременно хватать баланса аккаунта и лимита ключа. Непогашенные резервы учитываются в обоих, поэтому параллельные вызовы не могут превысить лимит, проверяя устаревшие итоговые значения. Даже неограниченный лимит ключа требует средств на аккаунте. Снижение лимита ниже уже занятой суммы блокирует новые запросы, но не удаляет прежние расходы и не отменяет принятые задачи. Приостановка блокирует новые вызовы и может быть отменена. Отзыв блокирует новые вызовы навсегда. Ключ с истёкшим сроком не может обращаться к API, даже если у владельца есть средства. Существующие задачи продолжаются и сохраняют ID ключа, которым были отправлены. Новый ключ начинает отдельную историю использования. Playground проходит аутентификацию через сессию вошедшего пользователя и отображается отдельно с источником `playground`; он никогда не раскрывает и не требует постоянного ключа пользователя. Графики аккаунта объединяют использование Playground и внешнего API. Графики с фильтром по ключу показывают только этот ключ; архивные и отозванные ключи остаются доступны в фильтрах истории. Сервер возвращает секретное значение открытым текстом только один раз; при последующих просмотрах восстановить его невозможно. [Аутентификация](/docs?page=auth&lang=ru) · [Тарификация](/docs?page=billing&lang=ru) ## Поля ограничений Для каждого ключа выбирается один лимит расходов и один период: за всё время, сутки, неделю или месяц. Период можно изменить; учитываемые окончательные списания пересчитываются для текущего периода без удаления истории. Суточный период сбрасывается в полночь UTC, недельный — в понедельник в 00:00 UTC, месячный — в первый день месяца в 00:00 UTC. Все непогашенные резервы учитываются в действующем лимите, включая принятые до границы периода: выполняющиеся запросы не позволяют обойти обновившийся лимит. При окончательном списании расход относится ко времени принятия запроса; период за всё время включает всю историю. Списки разрешённых моделей, RPM, адреса IPv4/IPv6 и списки разрешённых CIDR добавляют независимые проверки. Пустой список моделей или IP означает отсутствие соответствующего ограничения; интерфейс указывает это явно. Поддерживаются оба семейства адресов. Общие ограничения платформы для аккаунта сохраняются. Остаток лимита ключа не является балансом аккаунта. Для корректного применения ограничений в промышленной среде необходимо правильно настроить доверенные прокси. GET /v1/key возвращает id, name, mask, enabled, revoked, limit, period, expiresAt, models, rpm, ips, spent, reserved и nextReset. Денежные поля ключа — числовые значения в USD. limit=null означает отсутствие лимита расходов ключа; limit=0 запрещает любые положительные расходы. Баланс аккаунта по-прежнему учитывается. Секретное значение не возвращается. --- # Ошибки и ограничения ```json {"error":{"code":"KEY_BUDGET_EXCEEDED","message":"This key has insufficient spending allowance.","request_id":"req_demo_error"}} ``` | HTTP | Код | Действия для восстановления | |---|---|---| | 422 | `INVALID_INPUT` | Исправьте входные данные перед повторной отправкой. | | 401 | `INVALID_API_KEY`, `API_KEY_INACTIVE` | Используйте действительные учётные данные. | | 403 | `MODEL_NOT_ALLOWED`, `IP_NOT_ALLOWED` | Проверьте ограничения ключа в консоли. | | 402 | `INSUFFICIENT_BALANCE`, `KEY_BUDGET_EXCEEDED` | Пополните баланс или измените разрешённый лимит. | | 404 | `MODEL_NOT_FOUND`, `REQUEST_NOT_FOUND` | Проверьте ID и принадлежность аккаунту. | | 409 | `IDEMPOTENCY_CONFLICT` | Используйте исходное тело запроса или создайте новый логический запрос. | | 409 | Запись выполняющегося запроса или `RESPONSE_CACHE_EXPIRED` | Дождитесь завершения или сверки; не оплачивайте дублирующую генерацию. | | 422 | `UNSUPPORTED_PARAMETER` | Исправьте выбор модели или входных данных. | | 429 | `RATE_LIMITED` | Соблюдайте Retry-After и уменьшите частоту запросов. | | 502/503 | `MODEL_UNAVAILABLE` | Проверьте состояние запроса перед повторной отправкой. | Сообщения очищаются от конфиденциальных данных; баланс поставщика, секреты продавца и исходные трассировки ошибок вышестоящего сервиса никогда не показываются. `X-Request-ID` помогает при диагностике. Неизвестные ошибки провайдера нельзя ошибочно выдавать за нарушения правил в отношении контента. После отправки заголовков SSE ошибка передаётся как событие ошибки SSE с последующим закрытием соединения, а не как второй HTTP-ответ. [Идемпотентность](/docs?page=tasks&lang=ru) · [Промышленная эксплуатация](/docs?page=production&lang=ru) --- # Интеграция для промышленной эксплуатации Этот справочник сопровождает реализацию сервиса; развёртывание и проверка интеграции — отдельные этапы. Перед запуском реального трафика проверьте в отдельной тестовой среде каждый маршрут провайдера, схему параметров, расчёт цены, учёт использования в потоке, подпись платежей и сценарий восстановления после сбоя. - Используйте серверные ключи, TLS, явные тайм-ауты, ограниченный по времени опрос и идентификаторы запросов. - Сохраняйте записи идемпотентности до отправки; оплачиваемый POST вышестоящему сервису никогда не повторяется вслепую после транспортного сбоя с неопределённым исходом. - Финансовый журнал кошелька, резервы, бюджеты ключей и зачисление платежей должны оставаться транзакционными в MySQL. Используйте DynamoDB для проекций событий и запросов и хранения журналов запросов в течение 30 дней, а не как конкурирующий источник истины для кошелька. - Отправляйте запрос напрямую после сохранения резерва и записи о захвате задачи исполнителем. Возвращайте асинхронную задачу только после получения её ID у вышестоящего сервиса. GET задачи опрашивает её состояние и выполняет окончательное списание; периодический процесс сверки проверяет задачи без активного наблюдения и повторяет неудавшиеся записи журналов. - По умолчанию журналы содержат только метаданные и истекают через 30 дней. Не сохраняйте в них промпты, содержимое чатов, входные файлы, сгенерированные файлы, заголовки Authorization или платёжные секреты. - Для финансового журнала и записей платежей действует отдельная политика хранения; удаление метаданных запросов не должно удалять финансовую историю. Требования к хранению в промышленной среде необходимо окончательно определить до запуска. - Настройте оповещения о накоплении задач сверки, зависших резервах, попытках повторного списания, задержках задач, накоплении повторных записей журналов, ошибках провайдеров и расхождениях платежей. Перед запуском настройте учётные данные, DNS/TLS и разрешения рабочей нагрузки и проверьте реальные интеграции. [Задачи](/docs?page=tasks&lang=ru) · [Тарификация](/docs?page=billing&lang=ru) MySQL отвечает за координацию задач, финансовые транзакции и записи ожидающих повторных попыток журналирования. Записывайте метаданные напрямую в DynamoDB с проекциями по аккаунту, ключу и времени, группировкой по датам и LOG_SHARDS; при запросах исключайте просроченные записи. Лёгкий процесс сверки опрашивает задачи без активного наблюдения и повторяет запись журналов. Веб-приложение, шлюз и процесс сверки развёртываются отдельно. Успешный ответ возвращает исходный URL результата без изменений. Доступность, срок действия и междоменные ограничения определяет исходный хост. Сохраните результат до истечения срока: повторный запрос завершённой задачи не продлевает ссылку. Метаданные и URL могут сохраняться для восстановления, но не сгенерированные файлы. --- # URL и временные входные файлы Если метаданные модели предусматривают `image`, передайте один HTTPS URL референсного изображения в поле верхнего уровня запроса генерации изображения или видео. Браузерные URL `blob:` недоступны сервису. Несколько референсных изображений не входят в текущий нормализованный запрос. Если модель поддерживает референсное изображение, передайте общедоступный HTTPS URL в `image`. Публичный API принимает URL, а не содержимое файлов. В Playground только вошедшие в систему и подтверждённые пользователи могут загружать временные изображения или видео в закрытый R2. XXXRouter не скачивает, не хранит и не размещает результаты генерации. Для временных файлов доступны PNG/JPEG/WebP до 10 MiB и MP4/WebM до 50 MiB, не более 20 попыток загрузки в минуту на аккаунт. Ссылки для чтения действуют максимум 1 час и не продлеваются. Правило истечения срока в один день удаляет объекты асинхронно и не гарантирует физическое удаление ровно через 24 часа. URL изображения можно использовать в поддерживаемом поле референса. URL видео подходят только моделям с заявленной поддержкой видео на входе и не могут передаваться как изображение. Внешние URL должны оставаться доступными во время обработки. ```json {"model":"seedream-4.5","prompt":"A small blue house","image":"https://example.com/reference.png","n":1,"size":"2048x2048"} ``` Успешный ответ возвращает исходный URL результата без изменений. Доступность, срок действия и междоменные ограничения определяет исходный хост. Сохраните результат до истечения срока: повторный запрос завершённой задачи не продлевает ссылку. Метаданные и URL могут сохраняться для восстановления, но не сгенерированные файлы. --- # Python — руководство по интеграции Запускайте эти примеры на своём сервере. Они используют HTTP-клиент среды выполнения языка, а не SDK платформы. До выполнения команды запросы не отправляются. Перед использованием модели необходимо настроить и включить. ## Подготовка Среда выполнения: **Python 3.11+**. [Скачать полный исходный код](/examples/python.py). Задайте базовый URL своего развёртывания, включив `/v1` ровно один раз, и серверный API-ключ. `https://xxxrouter.com` — официальный домен сервиса; для локального тестирования замените его на origin своего развёртывания. Сервис пока не развёрнут, а проверка и сертификация интеграции не завершены. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Запуск одного сценария `chat` читает фрагменты SSE и окончательные данные об использовании; `image` получает синхронное изображение; `async-image` и `video` обрабатывают HTTP 202 и опрашивают возвращённую задачу. Опрос ограничен примерно пятью минутами плюс временем уже выполняющегося чтения; после тайм-аута задача сохраняется для последующих запросов. ```sh python3 python.py chat # python3 python.py image # python3 python.py async-image # python3 python.py video ``` ## Идемпотентность и обработка ошибок Новый запуск создаёт новый ключ идемпотентности и может привести к новому списанию. В приложении сохраняйте этот ключ вместе с точным телом запроса до отправки. Задавайте `XXXROUTER_REQUEST_ID` только для возобновления того же логического запроса с той же моделью, телом, API-ключом и источником. После транспортного сбоя не запускайте запрос заново с новым ключом. Эти клиенты отправляют POST генерации один раз и никогда не повторяют его автоматически. Сохраняйте `X-Request-ID` из ответа. При HTTP-ошибках тело ответа сохраняется для диагностики. Задача с `status=unknown` прекращает автоматический опрос и требует последующего чтения или сверки; она не считается успешным или бесплатным запросом. Событие ошибки потока или отсутствие `[DONE]` означает прерывание. Отсутствие данных об использовании не означает нулевую стоимость. Разрыв соединения не отменяет уже принятую работу. Не записывайте API-ключи или подписанные URL медиафайлов в журналы. ## Входные изображения и файлы результатов Если модель поддерживает референсное изображение, передайте общедоступный HTTPS URL в `image`. Публичный API принимает URL, а не содержимое файлов. В Playground только вошедшие в систему и подтверждённые пользователи могут загружать временные изображения или видео в закрытый R2. XXXRouter не скачивает, не хранит и не размещает результаты генерации. Успешный ответ возвращает исходный URL результата без изменений. Доступность, срок действия и междоменные ограничения определяет исходный хост. Сохраните результат до истечения срока: повторный запрос завершённой задачи не продлевает ссылку. Метаданные и URL могут сохраняться для восстановления, но не сгенерированные файлы. ## Полный пример Этот код — пример интеграции, а не подтверждение того, что реальная модель или платёжный канал включены. Не включайте его в браузерную сборку. ```python """Python 3.11+, standard library only. No automatic generation retries.""" import json import os import sys import time import urllib.error import urllib.request import uuid BASE = os.environ['XXXROUTER_BASE_URL'].rstrip('/') # Include /v1 exactly once. KEY = os.environ['XXXROUTER_API_KEY'] MODE = sys.argv[1] if len(sys.argv) > 1 else 'chat' CASES = { 'chat': ('chat/completions', {'model': 'deepseek-v4-flash', 'messages': [{'role': 'user', 'content': 'Explain HTTP in one sentence.'}], 'max_tokens': 256, 'stream': True, 'stream_options': {'include_usage': True}}), 'image': ('images/generations', {'model': 'seedream-4.5', 'prompt': 'A ceramic cup in natural light', 'size': '2048x2048', 'n': 1}), 'async-image': ('images/generations', {'model': 'nano-banana-pro', 'prompt': 'A ceramic cup in natural light', 'size': '1024x1024', 'n': 1}), 'video': ('videos/generations', {'model': 'kling-3.0', 'prompt': 'A slow camera move across a mountain landscape', 'duration': 5, 'aspect_ratio': '16:9'}), } class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None opener = urllib.request.build_opener(NoRedirect) def request(path, payload=None, idem=None): headers = {'Authorization': 'Bearer ' + KEY} if idem: headers['Idempotency-Key'] = idem data = None if payload is not None: headers['Content-Type'] = 'application/json' data = json.dumps(payload).encode() req = urllib.request.Request(BASE + '/' + path, data=data, headers=headers) try: return opener.open(req, timeout=300 if payload is not None else 30) except urllib.error.HTTPError as error: # Read the structured failure: an uncertain request may already have a hold. print('HTTP', error.code, 'request_id=', error.headers.get('X-Request-ID'), file=sys.stderr) print(error.read().decode(), file=sys.stderr) raise def stream(response): event = [] def consume(): data = '\n'.join(event) if data == '[DONE]': return True if data: chunk = json.loads(data) if chunk.get('error'): raise RuntimeError(chunk['error']) for choice in chunk.get('choices', []): print(choice.get('delta', {}).get('content') or '', end='', flush=True) if chunk.get('usage'): print('\nusage:', chunk['usage']) return False for raw in response: line = raw.decode('utf-8').rstrip('\r\n') if line == '': if consume(): return event.clear() elif line.startswith('data:'): event.append(line[5:].lstrip(' ')) if event and consume(): return raise RuntimeError('Stream interrupted. Keep request ID and idempotency key; do not create another submission.') def finish(result): deadline = time.monotonic() + 300 while result.get('task_id') and result.get('status') not in ('succeeded', 'failed', 'unknown'): if time.monotonic() >= deadline: print(result) raise TimeoutError('Keep task_id and query later; do not resubmit.') time.sleep(2) with request('tasks/' + result['task_id']) as response: result = json.load(response) print(json.dumps(result, indent=2)) if result.get('status') == 'unknown': raise RuntimeError('Outcome unconfirmed. Retain IDs and wait for reconciliation; do not resubmit.') if result.get('status') == 'failed': raise RuntimeError('Generation failed. Inspect error and billing before a new logical request.') if __name__ == '__main__': path, payload = CASES[MODE] idem = os.environ.get('XXXROUTER_REQUEST_ID') or str(uuid.uuid4()) print('idempotency_key=', idem, file=sys.stderr) # Persist the key with this exact payload before submitting in a real application. with request(path, payload, idem) as response: print('request_id=', response.headers.get('X-Request-ID'), file=sys.stderr) if 'text/event-stream' in response.headers.get('Content-Type', ''): stream(response) else: finish(json.load(response)) ``` ## Связанные справочные материалы [chat](/docs?page=chat&lang=ru) · [images](/docs?page=images&lang=ru) · [videos](/docs?page=videos&lang=ru) · [tasks](/docs?page=tasks&lang=ru) · [uploads](/docs?page=uploads&lang=ru) · [errors](/docs?page=errors&lang=ru) [Документация среды выполнения](https://docs.python.org/3/library/urllib.request.html) · 2026-10-26 --- # TypeScript / Node.js — руководство по интеграции Запускайте эти примеры на своём сервере. Они используют HTTP-клиент среды выполнения языка, а не SDK платформы. До выполнения команды запросы не отправляются. Перед использованием модели необходимо настроить и включить. ## Подготовка Среда выполнения: **Node.js 22.18+**. [Скачать полный исходный код](/examples/typescript.ts). Задайте базовый URL своего развёртывания, включив `/v1` ровно один раз, и серверный API-ключ. `https://xxxrouter.com` — официальный домен сервиса; для локального тестирования замените его на origin своего развёртывания. Сервис пока не развёрнут, а проверка и сертификация интеграции не завершены. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Запуск одного сценария `chat` читает фрагменты SSE и окончательные данные об использовании; `image` получает синхронное изображение; `async-image` и `video` обрабатывают HTTP 202 и опрашивают возвращённую задачу. Опрос ограничен примерно пятью минутами плюс временем уже выполняющегося чтения; после тайм-аута задача сохраняется для последующих запросов. ```sh node --experimental-strip-types typescript.ts chat # node --experimental-strip-types typescript.ts image # node --experimental-strip-types typescript.ts async-image # node --experimental-strip-types typescript.ts video ``` ## Идемпотентность и обработка ошибок Новый запуск создаёт новый ключ идемпотентности и может привести к новому списанию. В приложении сохраняйте этот ключ вместе с точным телом запроса до отправки. Задавайте `XXXROUTER_REQUEST_ID` только для возобновления того же логического запроса с той же моделью, телом, API-ключом и источником. После транспортного сбоя не запускайте запрос заново с новым ключом. Эти клиенты отправляют POST генерации один раз и никогда не повторяют его автоматически. Сохраняйте `X-Request-ID` из ответа. При HTTP-ошибках тело ответа сохраняется для диагностики. Задача с `status=unknown` прекращает автоматический опрос и требует последующего чтения или сверки; она не считается успешным или бесплатным запросом. Событие ошибки потока или отсутствие `[DONE]` означает прерывание. Отсутствие данных об использовании не означает нулевую стоимость. Разрыв соединения не отменяет уже принятую работу. Не записывайте API-ключи или подписанные URL медиафайлов в журналы. ## Входные изображения и файлы результатов Если модель поддерживает референсное изображение, передайте общедоступный HTTPS URL в `image`. Публичный API принимает URL, а не содержимое файлов. В Playground только вошедшие в систему и подтверждённые пользователи могут загружать временные изображения или видео в закрытый R2. XXXRouter не скачивает, не хранит и не размещает результаты генерации. Успешный ответ возвращает исходный URL результата без изменений. Доступность, срок действия и междоменные ограничения определяет исходный хост. Сохраните результат до истечения срока: повторный запрос завершённой задачи не продлевает ссылку. Метаданные и URL могут сохраняться для восстановления, но не сгенерированные файлы. ## Полный пример Этот код — пример интеграции, а не подтверждение того, что реальная модель или платёжный канал включены. Не включайте его в браузерную сборку. ```typescript // Node.js 22.18+; run: node --experimental-strip-types typescript.ts chat // Uses native fetch, including explicit HTTP and SSE error handling. import { randomUUID } from 'node:crypto'; type Result = { status?: string; task_id?: string; error?: unknown; [key: string]: any }; const base = process.env.XXXROUTER_BASE_URL?.replace(/\/$/, ''); // Include /v1. const key = process.env.XXXROUTER_API_KEY; if (!base || !key) throw new Error('Set XXXROUTER_BASE_URL and XXXROUTER_API_KEY'); const cases: Record]> = { chat: ['chat/completions', { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'Explain HTTP in one sentence.' }], max_tokens: 256, stream: true, stream_options: { include_usage: true } }], image: ['images/generations', { model: 'seedream-4.5', prompt: 'A ceramic cup in natural light', size: '2048x2048', n: 1 }], 'async-image': ['images/generations', { model: 'nano-banana-pro', prompt: 'A ceramic cup in natural light', size: '1024x1024', n: 1 }], video: ['videos/generations', { model: 'kling-3.0', prompt: 'A slow camera move across a mountain landscape', duration: 5, aspect_ratio: '16:9' }], }; async function request(path: string, payload?: unknown, idem?: string): Promise { const headers: Record = { Authorization: `Bearer ${key}` }; if (idem) headers['Idempotency-Key'] = idem; if (payload) headers['Content-Type'] = 'application/json'; const response = await fetch(`${base}/${path}`, { method: payload ? 'POST' : 'GET', headers, redirect: 'error', body: payload ? JSON.stringify(payload) : undefined, signal: AbortSignal.timeout(payload ? 300_000 : 30_000), }); console.error('request_id=', response.headers.get('X-Request-ID')); if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); return response; } async function stream(response: Response): Promise { if (!response.body) throw new Error('Empty response body'); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ''; let data: string[] = []; const consume = (): boolean => { const value = data.join('\n'); data = []; if (value === '[DONE]') return true; if (!value) return false; const chunk = JSON.parse(value); if (chunk.error) throw new Error(JSON.stringify(chunk.error)); for (const choice of chunk.choices ?? []) process.stdout.write(choice.delta?.content ?? ''); if (chunk.usage) console.log('\nusage:', chunk.usage); return false; }; try { while (true) { const { value, done } = await reader.read(); buffer += value ?? ''; let end: number; while ((end = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, end).replace(/\r$/, ''); buffer = buffer.slice(end + 1); if (!line && consume()) return; if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, '')); } if (done) break; } if (buffer.startsWith('data:')) data.push(buffer.slice(5).trimStart()); if (consume()) return; throw new Error('Stream interrupted. Keep request ID and idempotency key; do not resubmit.'); } finally { await reader.cancel(); reader.releaseLock(); } } async function finish(result: Result): Promise { const deadline = Date.now() + 300_000; while (result.task_id && !['succeeded', 'failed', 'unknown'].includes(result.status ?? '')) { if (Date.now() >= deadline) throw new Error(`Query task ${result.task_id} later; do not resubmit`); await new Promise(resolve => setTimeout(resolve, 2000)); result = await (await request(`tasks/${encodeURIComponent(result.task_id)}`)).json(); } console.log(result); if (result.status === 'unknown') throw new Error('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (result.status === 'failed') throw new Error('Generation failed; inspect error and billing.'); } const [path, payload] = cases[process.argv[2] ?? 'chat']; const idem = process.env.XXXROUTER_REQUEST_ID || randomUUID(); console.error('idempotency_key=', idem); // Persist alongside the exact payload before submitting. const response = await request(path, payload, idem); if (response.headers.get('content-type')?.includes('text/event-stream')) await stream(response); else await finish(await response.json()); ``` ## Связанные справочные материалы [chat](/docs?page=chat&lang=ru) · [images](/docs?page=images&lang=ru) · [videos](/docs?page=videos&lang=ru) · [tasks](/docs?page=tasks&lang=ru) · [uploads](/docs?page=uploads&lang=ru) · [errors](/docs?page=errors&lang=ru) [Документация среды выполнения](https://nodejs.org/api/globals.html#fetch) · 2026-10-26 --- # Go — руководство по интеграции Запускайте эти примеры на своём сервере. Они используют HTTP-клиент среды выполнения языка, а не SDK платформы. До выполнения команды запросы не отправляются. Перед использованием модели необходимо настроить и включить. ## Подготовка Среда выполнения: **Go 1.22+**. [Скачать полный исходный код](/examples/go.go). Задайте базовый URL своего развёртывания, включив `/v1` ровно один раз, и серверный API-ключ. `https://xxxrouter.com` — официальный домен сервиса; для локального тестирования замените его на origin своего развёртывания. Сервис пока не развёрнут, а проверка и сертификация интеграции не завершены. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Запуск одного сценария `chat` читает фрагменты SSE и окончательные данные об использовании; `image` получает синхронное изображение; `async-image` и `video` обрабатывают HTTP 202 и опрашивают возвращённую задачу. Опрос ограничен примерно пятью минутами плюс временем уже выполняющегося чтения; после тайм-аута задача сохраняется для последующих запросов. ```sh go run go.go chat # go run go.go image # go run go.go async-image # go run go.go video ``` ## Идемпотентность и обработка ошибок Новый запуск создаёт новый ключ идемпотентности и может привести к новому списанию. В приложении сохраняйте этот ключ вместе с точным телом запроса до отправки. Задавайте `XXXROUTER_REQUEST_ID` только для возобновления того же логического запроса с той же моделью, телом, API-ключом и источником. После транспортного сбоя не запускайте запрос заново с новым ключом. Эти клиенты отправляют POST генерации один раз и никогда не повторяют его автоматически. Сохраняйте `X-Request-ID` из ответа. При HTTP-ошибках тело ответа сохраняется для диагностики. Задача с `status=unknown` прекращает автоматический опрос и требует последующего чтения или сверки; она не считается успешным или бесплатным запросом. Событие ошибки потока или отсутствие `[DONE]` означает прерывание. Отсутствие данных об использовании не означает нулевую стоимость. Разрыв соединения не отменяет уже принятую работу. Не записывайте API-ключи или подписанные URL медиафайлов в журналы. ## Входные изображения и файлы результатов Если модель поддерживает референсное изображение, передайте общедоступный HTTPS URL в `image`. Публичный API принимает URL, а не содержимое файлов. В Playground только вошедшие в систему и подтверждённые пользователи могут загружать временные изображения или видео в закрытый R2. XXXRouter не скачивает, не хранит и не размещает результаты генерации. Успешный ответ возвращает исходный URL результата без изменений. Доступность, срок действия и междоменные ограничения определяет исходный хост. Сохраните результат до истечения срока: повторный запрос завершённой задачи не продлевает ссылку. Метаданные и URL могут сохраняться для восстановления, но не сгенерированные файлы. ## Полный пример Этот код — пример интеграции, а не подтверждение того, что реальная модель или платёжный канал включены. Не включайте его в браузерную сборку. ```go // Go 1.22+, standard library only. Run: go run go.go chat package main import ( "bufio" "bytes" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) type Object = map[string]any var base = strings.TrimRight(os.Getenv("XXXROUTER_BASE_URL"), "/") // Include /v1. var key = os.Getenv("XXXROUTER_API_KEY") var client = &http.Client{Timeout: 300 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} func request(path string, payload Object, idem string) (*http.Response, error) { var body io.Reader method := "GET" if payload != nil { raw, err := json.Marshal(payload) if err != nil { return nil, err } body = bytes.NewReader(raw) method = "POST" } req, err := http.NewRequest(method, base+"/"+path, body) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) if payload != nil { req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", idem) } response, err := client.Do(req) if err != nil { return nil, err } fmt.Fprintln(os.Stderr, "request_id=", response.Header.Get("X-Request-ID")) if response.StatusCode < 200 || response.StatusCode >= 300 { defer response.Body.Close() raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20)) return nil, fmt.Errorf("HTTP %d: %s", response.StatusCode, raw) } return response, nil } func decode(response *http.Response) (Object, error) { defer response.Body.Close() var result Object err := json.NewDecoder(response.Body).Decode(&result) return result, err } func stream(response *http.Response) error { defer response.Body.Close() scanner := bufio.NewScanner(response.Body) scanner.Buffer(make([]byte, 4096), 2<<20) var parts []string consume := func() (bool, error) { data := strings.Join(parts, "\n") parts = nil if data == "[DONE]" { return true, nil } if data == "" { return false, nil } var chunk struct { Error json.RawMessage `json:"error"` Usage json.RawMessage `json:"usage"` Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` } if err := json.Unmarshal([]byte(data), &chunk); err != nil { return false, err } if len(chunk.Error) > 0 && string(chunk.Error) != "null" { return false, fmt.Errorf("stream error: %s", chunk.Error) } for _, choice := range chunk.Choices { fmt.Print(choice.Delta.Content) } if len(chunk.Usage) > 0 && string(chunk.Usage) != "null" { fmt.Printf("\nusage: %s\n", chunk.Usage) } return false, nil } for scanner.Scan() { line := scanner.Text() if line == "" { done, err := consume() if err != nil || done { return err } } if strings.HasPrefix(line, "data:") { parts = append(parts, strings.TrimPrefix(line[5:], " ")) } } if err := scanner.Err(); err != nil { return err } done, err := consume() if err != nil || done { return err } return fmt.Errorf("stream interrupted; keep request ID and idempotency key, do not resubmit") } func finish(result Object) error { deadline := time.Now().Add(5 * time.Minute) for { task, hasTask := result["task_id"].(string) status, _ := result["status"].(string) if !hasTask || status == "succeeded" || status == "failed" || status == "unknown" { break } if time.Now().After(deadline) { return fmt.Errorf("query task %s later; do not resubmit", task) } time.Sleep(2 * time.Second) response, err := request("tasks/"+task, nil, "") if err != nil { return err } result, err = decode(response) if err != nil { return err } } raw, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(raw)) if result["status"] == "unknown" { return fmt.Errorf("outcome unconfirmed; retain IDs and wait for reconciliation") } if result["status"] == "failed" { return fmt.Errorf("generation failed; inspect error and billing") } return nil } func run() error { if base == "" || key == "" { return fmt.Errorf("set XXXROUTER_BASE_URL and XXXROUTER_API_KEY") } mode := "chat" if len(os.Args) > 1 { mode = os.Args[1] } path := "images/generations" var payload Object switch mode { case "chat": path = "chat/completions" payload = Object{"model": "deepseek-v4-flash", "messages": []Object{{"role": "user", "content": "Explain HTTP in one sentence."}}, "max_tokens": 256, "stream": true, "stream_options": Object{"include_usage": true}} case "image": payload = Object{"model": "seedream-4.5", "prompt": "A ceramic cup in natural light", "size": "2048x2048", "n": 1} case "async-image": payload = Object{"model": "nano-banana-pro", "prompt": "A ceramic cup in natural light", "size": "1024x1024", "n": 1} case "video": path = "videos/generations" payload = Object{"model": "kling-3.0", "prompt": "A slow camera move across a mountain landscape", "duration": 5, "aspect_ratio": "16:9"} default: return fmt.Errorf("choose chat, image, async-image or video") } idem := os.Getenv("XXXROUTER_REQUEST_ID") if idem == "" { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return err } idem = hex.EncodeToString(b) } fmt.Fprintln(os.Stderr, "idempotency_key=", idem) // Persist with the exact payload before submission. response, err := request(path, payload, idem) if err != nil { return err } if strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { return stream(response) } result, err := decode(response) if err != nil { return err } return finish(result) } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } ``` ## Связанные справочные материалы [chat](/docs?page=chat&lang=ru) · [images](/docs?page=images&lang=ru) · [videos](/docs?page=videos&lang=ru) · [tasks](/docs?page=tasks&lang=ru) · [uploads](/docs?page=uploads&lang=ru) · [errors](/docs?page=errors&lang=ru) [Документация среды выполнения](https://pkg.go.dev/net/http) · 2026-10-26 --- # PHP — руководство по интеграции Запускайте эти примеры на своём сервере. Они используют HTTP-клиент среды выполнения языка, а не SDK платформы. До выполнения команды запросы не отправляются. Перед использованием модели необходимо настроить и включить. ## Подготовка Среда выполнения: **PHP 8.1+ + ext-curl**. [Скачать полный исходный код](/examples/php.php). Задайте базовый URL своего развёртывания, включив `/v1` ровно один раз, и серверный API-ключ. `https://xxxrouter.com` — официальный домен сервиса; для локального тестирования замените его на origin своего развёртывания. Сервис пока не развёрнут, а проверка и сертификация интеграции не завершены. ```sh export XXXROUTER_BASE_URL='https://xxxrouter.com/v1' export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY' ``` ## Запуск одного сценария `chat` читает фрагменты SSE и окончательные данные об использовании; `image` получает синхронное изображение; `async-image` и `video` обрабатывают HTTP 202 и опрашивают возвращённую задачу. Опрос ограничен примерно пятью минутами плюс временем уже выполняющегося чтения; после тайм-аута задача сохраняется для последующих запросов. ```sh php php.php chat # php php.php image # php php.php async-image # php php.php video ``` ## Идемпотентность и обработка ошибок Новый запуск создаёт новый ключ идемпотентности и может привести к новому списанию. В приложении сохраняйте этот ключ вместе с точным телом запроса до отправки. Задавайте `XXXROUTER_REQUEST_ID` только для возобновления того же логического запроса с той же моделью, телом, API-ключом и источником. После транспортного сбоя не запускайте запрос заново с новым ключом. Эти клиенты отправляют POST генерации один раз и никогда не повторяют его автоматически. Сохраняйте `X-Request-ID` из ответа. При HTTP-ошибках тело ответа сохраняется для диагностики. Задача с `status=unknown` прекращает автоматический опрос и требует последующего чтения или сверки; она не считается успешным или бесплатным запросом. Событие ошибки потока или отсутствие `[DONE]` означает прерывание. Отсутствие данных об использовании не означает нулевую стоимость. Разрыв соединения не отменяет уже принятую работу. Не записывайте API-ключи или подписанные URL медиафайлов в журналы. ## Входные изображения и файлы результатов Если модель поддерживает референсное изображение, передайте общедоступный HTTPS URL в `image`. Публичный API принимает URL, а не содержимое файлов. В Playground только вошедшие в систему и подтверждённые пользователи могут загружать временные изображения или видео в закрытый R2. XXXRouter не скачивает, не хранит и не размещает результаты генерации. Успешный ответ возвращает исходный URL результата без изменений. Доступность, срок действия и междоменные ограничения определяет исходный хост. Сохраните результат до истечения срока: повторный запрос завершённой задачи не продлевает ссылку. Метаданные и URL могут сохраняться для восстановления, но не сгенерированные файлы. ## Полный пример Этот код — пример интеграции, а не подтверждение того, что реальная модель или платёжный канал включены. Не включайте его в браузерную сборку. ```php getMessage(); } }; $curl = curl_init($base . '/' . $path); $options = [ CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $payload !== null ? 300 : 30, CURLOPT_HEADERFUNCTION => function ($handle, string $line) use (&$contentType, &$requestId): int { if (stripos($line, 'Content-Type:') === 0) $contentType = trim(substr($line, 13)); if (stripos($line, 'X-Request-ID:') === 0) $requestId = trim(substr($line, 13)); return strlen($line); }, CURLOPT_WRITEFUNCTION => function ($handle, string $bytes) use (&$contentType, &$raw, &$buffer, &$data, &$done, &$streamError, $consume): int { if (!str_contains($contentType, 'text/event-stream')) { $raw .= $bytes; return strlen($bytes); } $buffer .= $bytes; while (($end = strpos($buffer, "\n")) !== false) { $line = rtrim(substr($buffer, 0, $end), "\r"); $buffer = substr($buffer, $end + 1); if ($line === '') $consume(); elseif (str_starts_with($line, 'data:')) $data[] = ltrim(substr($line, 5), ' '); } return strlen($bytes); }, ]; if ($payload !== null) { $headers[] = 'Content-Type: application/json'; $headers[] = 'Idempotency-Key: ' . $idem; $options[CURLOPT_POST] = true; $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_THROW_ON_ERROR); } $options[CURLOPT_HTTPHEADER] = $headers; curl_setopt_array($curl, $options); try { $ok = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); fwrite(STDERR, 'request_id=' . $requestId . "\n"); if ($ok === false) throw new RuntimeException('Transport error; retain IDs, do not resubmit: ' . curl_error($curl)); if ($status < 200 || $status >= 300) throw new RuntimeException('HTTP ' . $status . ': ' . $raw); if (str_contains($contentType, 'text/event-stream')) { if (str_starts_with($buffer, 'data:')) $data[] = ltrim(substr($buffer, 5), ' '); $consume(); if ($streamError !== null) throw new RuntimeException($streamError); if (!$done) throw new RuntimeException('Stream interrupted; retain request ID and idempotency key.'); return ['stream_complete' => true]; } return json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } finally { curl_close($curl); } } $cases = [ 'chat' => ['chat/completions', ['model'=>'deepseek-v4-flash','messages'=>[['role'=>'user','content'=>'Explain HTTP in one sentence.']],'max_tokens'=>256,'stream'=>true,'stream_options'=>['include_usage'=>true]]], 'image' => ['images/generations', ['model'=>'seedream-4.5','prompt'=>'A ceramic cup in natural light','size'=>'2048x2048','n'=>1]], 'async-image' => ['images/generations', ['model'=>'nano-banana-pro','prompt'=>'A ceramic cup in natural light','size'=>'1024x1024','n'=>1]], 'video' => ['videos/generations', ['model'=>'kling-3.0','prompt'=>'A slow camera move across a mountain landscape','duration'=>5,'aspect_ratio'=>'16:9']], ]; $mode = $argv[1] ?? 'chat'; if (!isset($cases[$mode])) throw new InvalidArgumentException('Choose chat, image, async-image or video'); [$path, $payload] = $cases[$mode]; $idem = getenv('XXXROUTER_REQUEST_ID') ?: bin2hex(random_bytes(16)); fwrite(STDERR, 'idempotency_key=' . $idem . "\n"); // Persist alongside exact payload before submission. $result = request($path, $payload, $idem); $deadline = microtime(true) + 300; while (isset($result['task_id']) && !in_array($result['status'], ['succeeded', 'failed', 'unknown'], true)) { if (microtime(true) >= $deadline) throw new RuntimeException('Query task ' . $result['task_id'] . ' later; do not resubmit.'); sleep(2); $result = request('tasks/' . rawurlencode($result['task_id'])); } echo json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . "\n"; if (($result['status'] ?? '') === 'unknown') throw new RuntimeException('Outcome unconfirmed; retain IDs and wait for reconciliation.'); if (($result['status'] ?? '') === 'failed') throw new RuntimeException('Generation failed; inspect error and billing.'); ``` ## Связанные справочные материалы [chat](/docs?page=chat&lang=ru) · [images](/docs?page=images&lang=ru) · [videos](/docs?page=videos&lang=ru) · [tasks](/docs?page=tasks&lang=ru) · [uploads](/docs?page=uploads&lang=ru) · [errors](/docs?page=errors&lang=ru) [Документация среды выполнения](https://www.php.net/manual/en/function.curl-exec.php) · 2026-10-26 --- # История изменений ## 0.1.0 — 2026-10-26 — Черновая спецификация реализации - Добавлены баланс аккаунта в USD и ограничения расходов для отдельных ключей. - Добавлены справочные материалы для разработчиков на en / zh-CN и машиночитаемый черновик OpenAPI. - Разделены непосредственные синхронные результаты генерации изображений и асинхронные задачи генерации изображений и видео. - Определён текстовый чат в стиле OpenAI с необязательным SSE. - Добавлены пробный бонус $0.20 со сроком действия 7 дней и платёжные страницы Payblis / NOWPayments. - Политика журналирования запросов по умолчанию: только метаданные, хранение 30 дней. Этот справочник описывает реализованные методы. Развёртывание, проверка рабочих маршрутов и приёмка платёжных каналов остаются отдельными требованиями к выпуску. [Быстрый старт](/docs?page=quickstart&lang=ru) ---