BazaarLinkBazaarLink
Sign in
DocsAPI ReferenceSDK ReferenceAgentic UsageAI Skills

BazaarLink Documentation

BazaarLink is a unified AI API gateway for Taiwan — providing access to hundreds of models from OpenAI, Anthropic, Google, Meta, and more through a single, OpenAI-compatible API endpoint.

AI Agent Skill File
Load our skill file into your AI assistant (Claude, Cursor, Copilot…) to give it full knowledge of the BazaarLink API:
Read https://bazaarlink.ai/skill.md and follow the instructions to integrate BazaarLink into your app.
Free models & rate limits
Looking for information about free models, per-minute request caps, and free credits? See Rate limits · FAQ

Pricing

BazaarLink prices model usage at zero markup (identical to each provider's official list price). Platform fees are charged at top-up (deposit): a 10% transaction fee, plus 5% Taiwan VAT on TWD channels. Billed in USD with TWD quotes and electronic unified invoices. Self-serve pay-as-you-go top-ups; enterprises may arrange monthly billing (Net-30, negotiable).

How it works

  • Consumption (debit): each API call is billed by actual token usage at the provider's official USD list price, deducted from your balance — zero markup, no extra fee on consumption.
  • Top-up (deposit): TWD is converted to USD at the real-time selling rate and added to your balance; a 10% transaction fee is charged at top-up.
  • • Credit card: an additional US$0.60 flat fee applies; a receipt is issued.
  • • TWD channels: 5% Taiwan VAT is added and a Taiwan electronic unified invoice is issued.
  • • Bank wire transfer: for larger or enterprise top-ups, contact us to arrange a wire transfer and custom invoicing.
  • Invoicing: electronic unified invoices are supported for Taiwan expense workflows; companies needing procurement or monthly billing can arrange enterprise terms (Net-30, negotiable).
Example
For a US$10.00 top-up: TWD channel = $10.00 + 10% fee US$1.00 + 5% VAT US$0.55 = US$11.55 (unified invoice issued); Credit card = $10.00 + fee US$1.00 + flat fee US$0.60 = US$11.60. After top-up, your US$10.00 balance is spent at official list prices with no further markup.

About the exchange rate

Foreign-exchange conversion uses the real-time rate; monthly billing uses the rate at billing (statement) time, while prepaid top-ups convert at the top-up-time rate. The rate and timestamp are retained with billing records.

Failed-request billing protection

If an upstream request fails without billable usage, BazaarLink automatically returns the entire reserved amount. You pay $0 for that attempt even if the stream had already started before it failed.

When you are not charged
No setting is required. This rule is enabled automatically for public inference and media APIs. BazaarLink may absorb a cost already charged by the upstream provider instead of passing that failed-request cost on to you.
  • The upstream cannot connect, rejects the request, or returns no usable response
  • A stream stops before a final usage record is received, even after partial content
  • The response has no usage data, or only an empty all-zero usage object

0 output tokens does not always mean free

If a request finishes normally and the provider returns valid usage, BazaarLink settles that usage. Do not decide whether a request was free from output tokens alone: a response with 0 output tokens can still be billed for input tokens or a valid upstream-reported cost. Check the returned usage.cost or the Activity record for the final charge.

Quick Start

Three Ways to Integrate

Approach
Best for
Start
Raw APIAny language, zero dependencies, full request control
OpenAI / Anthropic SDKAlready on an official SDK — swap base URL and key only
Agent frameworksLangChain, Vercel AI SDK, CrewAI and other agent apps

Get started in under 5 minutes. BazaarLink is fully compatible with the OpenAI SDK — just change the

Base URL

https://bazaarlink.ai/api/v1

Using the OpenAI SDK

BazaarLink is fully compatible with the OpenAI SDK. Just change the base URL and API key — all other code stays the same.

from openai import OpenAI

client = OpenAI(
    base_url="https://bazaarlink.ai/api/v1",
    api_key="sk-bl-YOUR_API_KEY",
)

completion = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[
        {"role": "user", "content": "What is the meaning of life?"}
    ],
)

print(completion.choices[0].message.content)
Need an API key?
Get your API key from the API Keys page. All keys start with sk-bl-.
Model ID Format — use provider/model-name
Always use the full provider/model format (e.g. openai/gpt-4.1). Common families (gpt-*, claude-*) are auto-prefixed on every endpoint, and chat/completions additionally resolves any unambiguous bare name from the catalog — but names that cannot be resolved return a 400 error, so the full form is the only guaranteed one.
✓ openai/gpt-4o   anthropic/claude-sonnet-4.6   google/gemini-2.5-flash
✗ gpt-4.1   claude-sonnet-4.6   gemini-2.5-flash

Bring Your Own Key (BYOK)

Attach your own upstream provider API keys (OpenAI- or Anthropic-compatible) to your account or organization — eligible requests then go upstream through your key, with seamless or strict fallback modes. Personal keys live on the BYOK tab of the keys page; organizations manage theirs in org settings. Go to BYOK settings →

Content filtering

Two-way content protection for your API traffic: requests carrying prompt-injection attempts are blocked (400), and sensitive data in prompts or responses (API keys, card numbers, national IDs, and more) is automatically redacted. Rules and an exemption allowlist are customizable, with usage statistics included. Go to content filter settings →

Migrate from OpenRouter

BazaarLink's API is OpenRouter-compatible — most integrations switch by changing two values: the base URL to https://bazaarlink.ai/api/v1 and the API key to a BazaarLink key starting with sk-bl-.

  1. Base URL: https://openrouter.ai/api/v1 → https://bazaarlink.ai/api/v1
  2. API key: sk-or-... → sk-bl-... (create one at /keys)
  3. Model IDs: same provider/model format (e.g. anthropic/claude-sonnet-4.6); full catalog at GET /api/v1/models
  4. models[] fallbacks, provider routing preferences, streaming, tool calling, and structured outputs use the same request shape
  from openai import OpenAI

  client = OpenAI(
-     base_url="https://openrouter.ai/api/v1",
-     api_key="sk-or-...",
+     base_url="https://bazaarlink.ai/api/v1",
+     api_key="sk-bl-...",
  )
Note
Billing is in USD with Taiwan e-invoices available. For OpenRouter-specific features (e.g. :nitro provider sorting), see the Model Variants and Provider Selection sections on the API Reference page for the equivalent behavior.

Authentication

All API requests require an Authorization header with your API key.

Authorization: Bearer sk-bl-YOUR_API_KEY

Get your API key from the dashboard. Keep your key secure — do not expose it in client-side code.

Security Note
Never expose API keys in client-side JavaScript. Always proxy requests through your backend server.

Optional Headers

HTTP-Referer
string
Your site URL, for usage tracking and analytics (optional)
X-Title
string
Your app name, shown in dashboards (optional)

Principles

BazaarLink is designed around three core principles:

1. Unified Interface

One API, one SDK, hundreds of models. Switch between OpenAI, Anthropic, Google Gemini, Meta Llama, and others without changing your code — just change the model ID.

2. Price Optimization

BazaarLink automatically routes to the most cost-effective provider for your chosen model. You pay only for what you use, billed in USD with full invoicing support.

3. High Availability

Automatic failover means if a provider goes down, your requests are seamlessly rerouted. No code changes, no downtime.

Multimodal

BazaarLink supports multimodal inputs — send images, audio, and files alongside text to models that support them. Content is passed through to the upstream provider.

Supported Modalities

Input
Description
Example Models
TextStandard text messagesAll models
ImagesURL or base64 data URI — PNG, JPEG, WebP, GIFopenai/gpt-5.3-codexanthropic/claude-opus-4.6google/gemini-3.1-pro-preview+143 more
Files / PDFsDocument via base64 data URI (`data:application/pdf;base64,...`)openai/gpt-5.3-codexanthropic/claude-opus-4.6google/gemini-3.1-pro-preview+72 more
AudioRaw base64 — no URL support. Requires `format` fieldgoogle/gemini-3.1-pro-previewxiaomi/mimo-v2.5google/gemini-3.1-flash-lite-preview+13 more
VideoURL (CDN) or base64 data URIgoogle/gemini-3.1-pro-previewqwen/qwen3.5-plus-02-15z-ai/glm-5v-turbo+37 more

Examples:

# Image — URL or base64 data URI
curl https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"google/gemini-2.5-flash","messages":[{"role":"user","content":[
        {"type":"text","text":"What is in this?"},
        {"type":"image_url","image_url":{"url":"https://example.com/photo.jpg"}}
      ]}]}'

# File / PDF — base64 data URI only, no URL
curl https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"google/gemini-2.5-flash","messages":[{"role":"user","content":[
        {"type":"file","file":{"filename":"doc.pdf","file_data":"data:application/pdf;base64,JVBER..."}},
        {"type":"text","text":"Summarize this."}
      ]}]}'

# Audio — raw base64, no URL. "format" is required
curl https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"google/gemini-2.5-flash","messages":[{"role":"user","content":[
        {"type":"text","text":"Transcribe this."},
        {"type":"input_audio","input_audio":{"data":"UklGRi...","format":"wav"}}
      ]}]}'

# Video — URL (CDN) or base64 data URI
curl https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"google/gemini-2.5-flash","messages":[{"role":"user","content":[
        {"type":"text","text":"Describe this video."},
        {"type":"video_url","video_url":{"url":"https://example.com/clip.mp4"}}
      ]}]}'

Sending Images

Use the content array format with image_url parts. Supported formats: PNG, JPEG, WebP, and GIF (including animated). You can include multiple images in a single message — each as a separate image_url part:

curl https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer sk-bl-YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-4o",
    "messages": [{"role":"user","content":[
      {"type":"text","text":"What is in this image?"},
      {"type":"image_url","image_url":{"url":"https://example.com/photo.jpg","detail":"auto"}}
    ]}]
  }'
Sending Images
Always include a text part alongside images. Text-first ordering (text part before image parts) is recommended for best compatibility across all providers.
Sending Images
Check the Models page for each model's supported input modalities. The modality column shows which inputs each model accepts.

Limits

BazaarLink enforces two independent limits: a rate limit on requests per minute, and a credit limit on account spending. Exceeding the rate limit returns HTTP 429; running out of credits returns HTTP 402.

Rate Limits

Rate limits are per user (not per key), measured in requests per minute (RPM). There is no daily cap. Tier is determined automatically by your account credit balance.

Tier
RPM
Daily Usage
Notes
Free (< $5 credits)20 RPMUnlimitedDevelopment & testing
Paid (≥ $5 credits)200 RPMUnlimitedProduction workloads

When a rate limit is exceeded you receive a 429 response with a Retry-After header. Implement exponential backoff when retrying requests.

Response Headers

Every successful response includes rate limit headers for client-side tracking:

X-RateLimit-Limit: 200        # Max requests per minute for your tier
X-RateLimit-Remaining: 198    # Remaining requests in current window
X-RateLimit-Reset: 1740000060 # Unix timestamp when the window resets
X-Request-Id: chatcmpl-abc123 # Unique request ID for debugging

Credit Limits

A 402 response means your account balance or a key's spending cap has hit zero — not that you're sending requests too fast. These responses carry no rate limit headers, and if the limit is hit mid-stream you'll receive an SSE error event instead of an HTTP status change.

402 Insufficient Credits
When your balance reaches $0, the API returns HTTP 402 with message "Insufficient credits. Please top up to continue." — monitor usage.cost in responses to track spending in real time.

Personal Circuit Breaker

A fixed 1-minute and 1-hour USD spend limit applied to all your API keys. New requests get HTTP 429 once a window threshold is hit; the window auto-resets on the clock boundary.

cbEnabled
boolean
Enabled
cbMinuteUsd
number | null
Per-minute USD limit · Use default
cbHourlyUsd
number | null
Per-hour USD limit · Use default
(inheriting defaults)
Values must be at least 0.01 (or empty for default)
Personal Circuit Breaker · Adjust

Image Generation

Generate images via /v1/chat/completions with modalities:["image"], or the OpenAI DALL·E-compatible /v1/images/generations.

curl -N https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"openai/gpt-5.4-image-2","messages":[{"role":"user","content":"a red cat on a sofa"}],"modalities":["image","text"],"stream":true}'

Full flow (streaming, image edits, SSE protocol, model list) →

Video Generation

Async 3-step flow (submit → poll → content). Video generation takes 30 seconds to 5 minutes.

curl https://bazaarlink.ai/api/v1/videos \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"alibaba/wan2.7-t2v","prompt":"a bird flying over mountains","duration":3}'
# → 202 { "id": "vjob_xxx", "status": "pending" }

Full flow (polling, download, task types, caveats) →

PDF Inputs

Send PDF documents directly in messages for models that support PDF input natively (e.g. Claude, Gemini). BazaarLink forwards the file straight to the model — billed as ordinary input tokens, no extra charge or processing step.

Supported Formats

  • PDF documents (text, images, tables, scanned)
  • Base64-encoded data URL (`data:application/pdf;base64,...`)
  • Multi-page documents
  • Password-free PDFs only
import base64

with open("document.pdf", "rb") as f:
    pdf_data = base64.b64encode(f.read()).decode()

response = client.chat.completions.create(
    model="anthropic/claude-sonnet-4.6",
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "file",
                "file": {
                    "filename": "document.pdf",
                    "file_data": f"data:application/pdf;base64,{pdf_data}",
                },
            },
            {"type": "text", "text": "Summarize this document."},
        ],
    }],
)

Video Inputs

Send video files to models that support video input for analysis, captioning, or questions about scenes and events. Works with a direct URL or a base64 data URI — a URL is more efficient for publicly accessible video; base64 is for local files or private video.

Supported Formats

MP4 (H.264)MPEGMOVWebM
response = client.chat.completions.create(
    model="google/gemini-2.5-flash",
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "video_url",
                "video_url": {"url": "https://example.com/video.mp4"},
            },
            {"type": "text", "text": "What is happening in this video?"},
        ],
    }],
)

Full API reference →

Management API Keys

Management keys are designed for programmatic key management. They can create, list, update, disable, and delete standard API keys — but cannot make AI model calls.

Note
Management keys cannot call AI models (chat/completions/messages/embeddings). Use a standard API key for model access.

Creating a Management Key

Go to the Management API Keys page and click "Create" — it's a separate page from your standard API Keys, not a type selector on the same page.

List Keys

GET https://bazaarlink.ai/api/v1/keys
Authorization: Bearer sk-bl-YOUR_MGMT_KEY

# Response
{
  "keys": [
    {
      "id": "clxyz123...",
      "name": "Production Key",
      "keyType": "standard",
      "keyPrefix": "sk-bl-abc1",
      "keySuffix": "XyZ9",
      "enabled": true,
      "spendLimitUsd": 10.00,
      "spendLimitPeriod": "month",
      "expiresAt": null,
      "createdAt": "2026-01-01T00:00:00.000Z",
      "lastUsed": "2026-03-01T12:34:56.000Z",
      "requestCount": 1234,
      "totalTokens": 5678901
    }
  ]
}

Create Sub-Key

POST https://bazaarlink.ai/api/v1/keys
Authorization: Bearer sk-bl-YOUR_MGMT_KEY
Content-Type: application/json

{
  "name": "Agent Key",
  "limit": 10.00,
  "limit_reset": "monthly",
  "expires_at": "2026-12-31T23:59:59Z"
}

# limit_reset: daily | weekly | monthly
# expires_at:  ISO 8601 datetime (optional)

# Response — save the key value, it won't be shown again
{
  "id": "clxyz789...",
  "name": "Agent Key",
  "key": "sk-bl-xyz789abcdef...",
  "keyType": "standard",
  "spendLimitUsd": 10.00,
  "spendLimitPeriod": "month",
  "expiresAt": "2026-12-31T23:59:59.000Z",
  "enabled": true,
  "createdAt": "2026-03-01T00:00:00.000Z"
}

Update Key

PATCH https://bazaarlink.ai/api/v1/keys/:id
Authorization: Bearer sk-bl-YOUR_MGMT_KEY
Content-Type: application/json

{"enabled": false}              # disable key
{"spendLimitUsd": 5, "spendLimitPeriod": "week"}  # set spend limit
{"spendLimitUsd": null}         # remove spend limit
# Response: {"updated": true}

Revoke Key

DELETE https://bazaarlink.ai/api/v1/keys/:id
Authorization: Bearer sk-bl-YOUR_MGMT_KEY

# Returns 204 No Content on success

Query Balance

GET https://bazaarlink.ai/api/v1/credits
Authorization: Bearer sk-bl-YOUR_MGMT_KEY

# Response
{
  "data": {
    "total_credits": 12.345,
    "total_usage": 3.210
  }
}

Query Usage

GET https://bazaarlink.ai/api/v1/usage?period=month
Authorization: Bearer sk-bl-YOUR_MGMT_KEY

# period: day | week | month | year

App Attribution

Identify your application in request headers to enable usage tracking, dashboard visibility, and fine-grained analytics.

Note
These headers are entirely optional and do not affect API functionality. However, setting them is recommended for debugging and usage attribution.

Available Headers

HeaderDescription
HTTP-RefererYour site URL, for usage tracking and analytics (optional)
X-TitleYour app name, shown in dashboards (optional)
from openai import OpenAI

client = OpenAI(
    base_url="https://bazaarlink.ai/api/v1",
    api_key="sk-bl-YOUR_KEY",
    default_headers={
        "HTTP-Referer": "https://yourapp.com",  # Optional: your site URL
        "X-Title": "My Application",             # Optional: your app name
    },
)

response = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[{"role": "user", "content": "Hello!"}],
)

Error Codes

Error response format

Model inference endpoints return an OpenAI-compatible error envelope. The type field may vary or be omitted on specialized endpoints; use the HTTP status and error.code for program logic instead of parsing the message.

{
  "error": {
    "message": "Insufficient credits. Please top up to continue.",
    "type": "invalid_request_error",
    "code": "insufficient_credits"
  }
}

HTTP status and error.code

Before streaming starts, the HTTP status identifies the broad failure class. error.code is either that numeric status or a stable string for failures that require a specific remedy. Prefer a string code when present, fall back to the HTTP status, and treat error.message as human-readable text.

Code
Name
Description
400Bad RequestMalformed request, empty messages array, or missing required fields
401UnauthorizedAPI key is missing, invalid, or disabled
402Payment RequiredInsufficient account credits, per-key spend limit reached, or monthly/weekly budget cap exceeded
403ForbiddenAccount is suspended or does not have permission
404Not FoundThe requested model, generation, key, or other resource does not exist
409ConflictThe resource is not in the required state, such as a video job that is not complete
410GoneThe requested model has been retired and must be replaced
413Payload Too LargeRequest body exceeds 10 MB; reduce content size or split the request
416Range Not SatisfiableThe requested byte range is invalid for generated video content
429Too Many RequestsRate limit exceeded; check Retry-After header before retrying
500Server ErrorInternal BazaarLink error
502Bad GatewayAll upstream providers failed; failover was attempted
503Service UnavailableNo upstream provider is configured for this model; contact admin
504Gateway TimeoutThe upstream connection or stream stalled and timed out

Machine-readable billing codes

A 402 response can represent different controls. These stable codes let clients show the correct next action.

Code
Description
budget_cap_reachedA weekly or monthly advisory budget cap was reached; raise or reset the cap.
credit_limit_exceededA monthly-billing organization's hard credit line was exhausted; contact billing.
insufficient_creditsA prepaid user or organization cannot reserve enough balance; add credits.
spend_limit_exceededThe API key reached its configured daily, weekly, or monthly spend limit.

Detailed error codes

When an API request fails, error.code tells you the specific reason. Two errors can both use HTTP 400 but require different fixes: unknown_model means the model name is wrong, while image_too_large means the image must be reduced. Use the table below to find the cause and next action.

Model and endpoint
Model lookup, lifecycle, pricing, modality, and endpoint compatibility errors.
Code
HTTP status
unknown_model400
invalid_model_id400
model_not_found404
model_retired410
model_endpoint_mismatch400
embedding_on_chat_endpoint400
model_not_priced400
invalid_modality_for_model400
Request and safety
Invalid parameters, context, tools, schemas, and content-safety refusals.
Code
HTTP status
missing_required_field400
unsupported_param400
max_tokens_invalid400
context_too_long400
tool_use_unsupported400
malformed_tool_messages400
invalid_response_format_schema400
invalid_tools_definition400
content_moderation403
content_filter403
unknown_4xx400
Image generation and editing
Image input, multipart editing, output, and image-pipeline errors.
Code
HTTP status
invalid_image_url400
input_images_not_supported400
invalid_content_type400
mask_not_supported400
unsupported_response_format400
missing_prompt400
missing_image400
too_many_images400
invalid_image_type400
image_too_large400
invalid_n400
pipeline_error502
no_images502
Upstream routing
Sanitized provider connectivity, authentication, throttling, and availability errors.
Code
HTTP status
upstream_unreachable502
upstream_auth_failed502
upstream_rate_limited429
upstream_unavailable502/503

Rate limits, budgets, and emergency brakes

These controls can reject an otherwise valid request. They are separate from provider failures and require different recovery actions.

Control
HTTP status
How to identify it
Request rate limit429Numeric code 429; use Retry-After and X-RateLimit-* headers.
Rate-limit penalty block429Numeric code 429 and a temporary restriction message; use Retry-After.
Global spend emergency brake503Numeric code 503, global spend-limit message, and Retry-After of 30 or 300 seconds.
Org/team/member/user spend brake429Numeric code 429 and a spend circuit-breaker message naming the affected scope.
Billing and budget controls402Use the stable billing string codes listed above.

Compatibility note: rate-limit and emergency-brake paths currently emit numeric error.code values. Do not assume undocumented string codes. Use HTTP status, Retry-After, and the documented response message until a stable string code is introduced.

Video and media resource state

Video validation commonly returns numeric code 400. A missing job returns 404, a retired model returns 410, unfinished video content returns 409, and an invalid video byte range returns 416. Poll until completion or correct the Range header before retrying.

Retry policy

Retry only failures that may recover without changing the request. When Retry-After is present, wait that long; otherwise use exponential backoff with jitter. Limit attempts and avoid layering manual retries on top of SDK automatic retries.

Retry with backoff
429, 502, 503, and 504. Respect Retry-After when provided. For generation requests, do not create a second job after an ambiguous network failure until you have checked the original job.
Fix before retrying
400, 401, 402, 403, 404, 409, 410, 413, and 416. Change the request, credentials, balance, permissions, resource state, or Range header first.

Handling Errors

import random
import time
from openai import OpenAI, APIStatusError

client = OpenAI(
    base_url="https://bazaarlink.ai/api/v1",
    api_key="sk-bl-YOUR_API_KEY",
    max_retries=0,  # Avoid double retries; this example handles them.
)

RETRYABLE = {429, 502, 503, 504}

for attempt in range(5):
    try:
        response = client.chat.completions.create(
            model="openai/gpt-4.1",
            messages=[{"role": "user", "content": "Hello!"}],
        )
        break
    except APIStatusError as error:
        if error.status_code not in RETRYABLE or attempt == 4:
            raise
        retry_after = error.response.headers.get("Retry-After")
        delay = (
            float(retry_after)
            if retry_after
            else min(8, 0.5 * (2 ** attempt)) + random.uniform(0, 0.25)
        )
        time.sleep(delay)

Streaming Error Formats

Errors that occur before any tokens are streamed return a standard HTTP error response with a JSON body.

After a stream has started, the HTTP response is already 200. Parse every SSE data frame and treat a top-level error or choices[0].finish_reason === "error" as a failed, incomplete response.

If the stream fails mid-flight, BazaarLink emits a final SSE event with a top-level error object followed by data: [DONE]. Chunks relayed verbatim from some upstreams may instead carry the error on the choice (choices[0].finish_reason === "error") — handle both.

// If the stream fails mid-flight, BazaarLink emits a final SSE event
// with a top-level "error" object, followed by data: [DONE]
data: {"error":{"message":"Upstream stream interrupted. The response is incomplete.","type":"upstream_error","code":502}}

data: [DONE]

// Chunks relayed verbatim from some upstreams may instead carry the error
// inline on the choice: choices[0].finish_reason === "error" with an
// "error" object ({ code, message }) on the choice — handle both shapes.
// Branch on error.code; error.type can vary by failure path.

Tool Calling

Tool calling (also known as function calling) lets models invoke external functions you define. The model decides when to call a tool and generates structured arguments — your code executes the function and returns results to continue the conversation.

Supported Models

Most frontier models support tool calling. Here are some popular choices:

Defining Tools

Each tool is a JSON object describing a function the model can call. The parameters field uses JSON Schema.

namerequired
string
Function name (a-z, A-Z, 0-9, underscores, dashes)
descriptionrequired
string
Clear description of when and how the function should be used
parametersrequired
object
JSON Schema object defining function parameters

tool_choice Options

Value
Behavior
"auto"Model decides whether to call a tool (default)
"none"Model will not call any tool
"required"Model must call at least one tool
{"type": "function", "function": {"name": "get_weather"}}Model must call the specified function

Complete Flow

Tool calling is a multi-turn process: (1) send the request with tools → (2) model returns tool_calls → (3) execute functions → (4) send results back → (5) model generates final response.

curl https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-4.1",
    "messages": [{"role":"user","content":"What is the weather in Taipei?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get current weather for a city",
        "parameters": {
          "type": "object",
          "properties": {
            "city": {"type": "string", "description": "City name"},
            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
          },
          "required": ["city"]
        }
      }
    }],
    "tool_choice": "auto"
  }'
# Response carries tool_calls — run get_weather() yourself, then send the
# result back with role:"tool" (same shape as the Python/TS steps 3-5) to
# get the model's final answer.

Parallel Tool Calls

Some models can call multiple tools in a single response. Handle each tool call and return all results:

# Model may return multiple tool_calls
if message.tool_calls:
    messages = [
        {"role": "user", "content": "Weather and time in Tokyo?"},
        message,
    ]

    for tool_call in message.tool_calls:
        # Execute each function
        if tool_call.function.name == "get_weather":
            result = {"temperature": 22, "condition": "Clear"}
        elif tool_call.function.name == "get_time":
            result = {"time": "2026-02-23T15:30:00+09:00"}

        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result),
        })

    # Send all results back at once
    final = client.chat.completions.create(
        model="openai/gpt-4.1",
        messages=messages,
        tools=tools,
    )
    print(final.choices[0].message.content)

Streaming Tool Calls

When streaming, tool calls arrive as partial deltas indexed by position — accumulate each delta's argument string by index until finish_reason becomes "tool_calls", which signals the call is complete.

# Streaming: tool_calls arrive as partial deltas indexed by position —
# accumulate function.arguments per index until finish_reason == "tool_calls".
stream = client.chat.completions.create(
    model="openai/gpt-4.1",
    messages=[{"role": "user", "content": "What's the weather in Taipei?"}],
    tools=tools,
    tool_choice="auto",
    stream=True,
)

tool_calls = {}
for chunk in stream:
    delta = chunk.choices[0].delta
    if delta.tool_calls:
        for tc in delta.tool_calls:
            entry = tool_calls.setdefault(tc.index, {"id": "", "name": "", "arguments": ""})
            if tc.id:
                entry["id"] = tc.id
            if tc.function.name:
                entry["name"] = tc.function.name
            if tc.function.arguments:
                entry["arguments"] += tc.function.arguments
    if chunk.choices[0].finish_reason == "tool_calls":
        for call in tool_calls.values():
            print(call["name"], json.loads(call["arguments"]))

Simple Agentic Loop

A generic pattern that keeps calling the model while it keeps requesting tools, and stops once it returns a final answer — use max_iterations to guard against infinite loops.

# Generic loop: keep calling the model while it keeps requesting tools,
# stop once it returns a plain answer. max_iterations guards against loops.
messages = [{"role": "user", "content": "What's the weather in Taipei, and what time is it there?"}]
max_iterations = 10

for _ in range(max_iterations):
    response = client.chat.completions.create(
        model="openai/gpt-4.1",
        messages=messages,
        tools=tools,
    )
    message = response.choices[0].message
    messages.append(message)

    if not message.tool_calls:
        break  # model gave a final answer

    for tool_call in message.tool_calls:
        args = json.loads(tool_call.function.arguments)
        result = TOOL_MAPPING[tool_call.function.name](**args)
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result),
        })
else:
    print("Warning: max_iterations reached without a final answer")

print(messages[-1].content)

Function Definition Best Practices

  • Use specific, descriptive names — get_weather_forecast rather than just weather.
  • Write a clear description of what the function does and when to use it — the model relies solely on this text to decide whether to call it.
  • Constrain values with enum where possible and include an example in the description to reduce malformed arguments.
  • Only mark fields required that are truly required — optional fields should be genuinely omittable.

Structured Output

Force the model to return valid JSON matching a schema. This is essential for building reliable applications that parse model outputs programmatically.

Method 1: response_format (JSON Schema)

to enforce strict JSON Schema compliance:

typerequired
string
Must be "json_schema"
json_schema.namerequired
string
A name for the schema (used for caching)
json_schema.strict
boolean
When true, guarantees exact schema compliance
json_schema.schemarequired
object
The JSON Schema definition
curl https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-4.1",
    "messages": [{"role":"user","content":"Review the movie Inception"}],
    "response_format": {
      "type": "json_schema",
      "json_schema": {
        "name": "movie_review",
        "strict": true,
        "schema": {
          "type": "object",
          "properties": {
            "title": {"type": "string"},
            "rating": {"type": "integer", "description": "Rating 1-10"},
            "summary": {"type": "string"},
            "pros": {"type": "array", "items": {"type": "string"}},
            "cons": {"type": "array", "items": {"type": "string"}}
          },
          "required": ["title", "rating", "summary", "pros", "cons"],
          "additionalProperties": false
        }
      }
    }
  }'

Tips

  • Use clear, descriptive property names — the model uses them as context.
  • Add descriptions to schema properties to guide the model.
  • Set strict: true for guaranteed schema compliance (may increase latency slightly).
  • Keep schemas simple — deeply nested schemas may reduce output quality.
  • Test with different models — some handle complex schemas better than others.

Assistant Prefill

Add a partial assistant message as the last item to request continuation on compatible model routes.

curl https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-4.6",
    "messages": [
      {"role":"user","content":"What is the capital of France?"},
      {"role":"assistant","content":"The capital of France is"}
    ]
  }'
# Model continues: " Paris, known for the Eiffel Tower..."
How it works
BazaarLink preserves and forwards the final assistant message. Continuation behavior is implemented by the selected upstream model and provider, so it is not guaranteed on every route.

Message Transforms

Automatically transform messages to fit within model context limits. When your messages exceed a model's context window, transforms intelligently condense the conversation by removing messages from the middle.

Auto
Models with a context window of 8,192 tokens or less apply middle-out automatically by default. To opt out, pass `transforms: []`. To enable for any model, pass `transforms: ["middle-out"]`.

Usage

// Enable middle-out on any model
{
  "model": "openai/gpt-4.1",
  "transforms": ["middle-out"],
  "messages": [
    { "role": "system", "content": "You are a helpful assistant." },
    ... // long conversation — middle will be trimmed to fit context
  ]
}

// Disable auto-trimming for small-context models
{ "transforms": [] }

Transform Types

Transform
Description
middle-outRemoves messages from the middle first, preserving the beginning (system prompt, context) and end (recent messages)

Default Behavior

Models with ≤8k context have middle-out enabled automatically. For larger context models, opt in explicitly. Anthropic Claude models also automatically enforce the 1,000-message limit regardless of the transforms setting.

Zero Data Retention

BazaarLink does not store your message content by default. This page describes how your data is handled. Suitable for applications processing sensitive data.

Current Data Handling

  • Message content: not stored by default, discarded from memory after processing
  • Billing metadata: token counts, timestamps, model IDs
  • Usage logs: request statistics only, no message content
  • Upstream forwarding: messages forwarded to upstream providers — subject to their privacy policies

Prompt Caching

Prompt caching reuses previously computed prompt tokens, significantly reducing cost and latency — especially for applications with large, repeated system prompts.

Note
BazaarLink automatically tracks cache savings and reflects them in billing. The `cached_tokens` field in the response shows actual cache hits; `cacheDiscount` shows the amount saved on that request.

How It Works

Whether caching needs configuration depends on the provider. OpenAI-family models cache long, repeated prompt prefixes automatically — no request changes needed. Claude (Anthropic) models only cache when the request carries an explicit cache_control breakpoint; BazaarLink does not add this for you, so a Claude request with no marker is never cached. BazaarLink forwards whatever cache markers you send as-is and reports the resulting cache read/write tokens in the usage response.

# OpenAI-family models: nothing to add, long repeated prefixes cache automatically.
response = client.chat.completions.create(
    model="openai/gpt-4.1",
    messages=[
        {"role": "system", "content": "You are an expert..."},  # cached automatically if long/repeated
        {"role": "user", "content": "Question here"},
    ],
)

# Check cache savings in the response usage
usage = response.usage
print(f"Prompt tokens: {usage.prompt_tokens}")
print(f"Cached tokens: {usage.prompt_tokens_details.cached_tokens}")
print(f"Cache savings: {usage.prompt_tokens_details.cached_tokens / usage.prompt_tokens * 100:.1f}%")
Claude needs an explicit cache_control marker
Add cache_control: {"type": "ephemeral"} to the content block you want cached, as shown below. Anthropic also enforces its own minimum prompt length before it will cache at all — short prompts silently skip caching even with the marker present. Check cached_tokens (OpenAI-shape) or cache_read_input_tokens / cache_creation_input_tokens (Anthropic-shape) in the response to confirm a hit.
# Claude models: you must mark the block to cache yourself.
response = client.chat.completions.create(
    model="anthropic/claude-sonnet-4.6",
    messages=[
        {
            "role": "system",
            "content": [
                {"type": "text", "text": "You are an expert...", "cache_control": {"type": "ephemeral"}}
            ],
        },  # BazaarLink does not add cache_control on your behalf
        {"role": "user", "content": "Question here"},
    ],
)

usage = response.usage
print(f"Cache read tokens: {getattr(usage, 'cache_read_input_tokens', 0)}")
print(f"Cache write tokens: {getattr(usage, 'cache_creation_input_tokens', 0)}")

Reasoning Tokens

Reasoning models (e.g., DeepSeek R1, o1 series) think internally before producing their final answer. These internal tokens are called reasoning tokens and are billed separately.

Note
BazaarLink reports reasoning tokens in `usage.completion_tokens_details.reasoning_tokens` and shows them separately in billing.

Reading Reasoning Tokens from Responses

response = client.chat.completions.create(
    model="deepseek/deepseek-r1",
    messages=[{"role": "user", "content": "Solve: if f(x) = x^2 + 3x, what is f(5)?"}],
)

# Read reasoning tokens from usage
usage = response.usage
print(f"Completion tokens: {usage.completion_tokens}")
if hasattr(usage, "completion_tokens_details"):
    details = usage.completion_tokens_details
    print(f"Reasoning tokens: {details.reasoning_tokens}")
    print(f"Output tokens: {details.accepted_prediction_tokens}")
const response = await client.chat.completions.create({
  model: "openai/o3-mini",
  messages: [{ role: "user", content: "Prove that sqrt(2) is irrational." }],
  // @ts-ignore - BazaarLink extension
  reasoning_effort: "high",  // low | medium | high
});

const usage = response.usage;
console.log("Reasoning tokens:", usage?.completion_tokens_details?.reasoning_tokens);

Thinking Mode Control

Some models support toggling their "thinking" mode. Thinking mode generates internal reasoning tokens before producing the final answer, improving quality at the cost of more tokens.

Model FamilyParameterDefault
qwen3-*enable_thinking: booleanfalse (platform default)
openai/o1, o3, o4-minireasoning_effort: "low" | "medium" | "high"medium
deepseek/deepseek-r1Always enabled (cannot disable)
# Qwen3: explicitly enable thinking mode
response = client.chat.completions.create(
    model="qwen/qwen3-32b",
    messages=[{"role": "user", "content": "Prove the Pythagorean theorem"}],
    extra_body={"enable_thinking": True},  # opt-in to thinking
)

# usage.completion_tokens_details.reasoning_tokens shows thinking token count

Unified reasoning Object (New Format)

BazaarLink also supports the unified reasoning object, which works across all model families with a single consistent API:

FieldValuesApplies to
reasoning.effort"xhigh" | "high" | "medium" | "low" | "none"OpenAI o-series, Grok
reasoning.max_tokensintegerAnthropic Claude, Gemini
reasoning.excludebooleanHide thinking from response (model still reasons)
// Claude extended thinking — specify thinking budget in tokens
const response = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-4.5",
  messages: [{ role: "user", content: "Prove the Pythagorean theorem" }],
  // @ts-ignore - BazaarLink extension
  reasoning: { max_tokens: 5000 },
});

// OpenAI o3 — specify effort level
const response2 = await client.chat.completions.create({
  model: "openai/o3",
  messages: [{ role: "user", content: "Solve this math problem..." }],
  // @ts-ignore - BazaarLink extension
  reasoning: { effort: "high" },
});

// Hide thinking content from response (model still thinks)
const response3 = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-4.5",
  messages: [{ role: "user", content: "What is 2+2?" }],
  // @ts-ignore - BazaarLink extension
  reasoning: { max_tokens: 2000, exclude: true },
});
Pricing
Thinking tokens are billed as completion tokens. Some providers charge a higher rate for thinking mode — Qwen3 costs 2x standard pricing when thinking is active. BazaarLink defaults Qwen3 to enable_thinking=false to avoid unexpected costs.

Latency & Performance

Optimizing AI API response latency is critical for user experience. Below are the key factors that affect latency in the BazaarLink architecture and best practices for optimization.

Note
BazaarLink records `duration_ms` (end-to-end latency) and `throughput` (tokens/sec) for every request — look them up via GET /api/v1/generation?id=..., or in the Activity Export CSV.

Factors That Affect Latency

  • Model size: larger models (70B+) are generally slower to generate
  • Provider load: varies across providers and time of day
  • Token count: higher max_tokens means longer completion time
  • Streaming vs. non-streaming: stream: true delivers the first token faster
  • Context length: very long contexts increase pre-processing time

Optimization Tips

  • Prefer streaming (stream: true) to improve perceived latency
  • Use the :nitro variant to select high-throughput providers
  • Choose smaller models (flash/mini/haiku) for latency-sensitive scenarios
  • Use provider.sort: "latency" to automatically select the lowest-latency provider
  • Enable prompt caching to reduce latency for repeated requests
import time

# Measure time to first token with streaming
start = time.time()
first_token_time = None

stream = client.chat.completions.create(
    model="google/gemini-2.5-flash",  # Fast model
    messages=[{"role": "user", "content": "Hello!"}],
    stream=True,
)

for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content and not first_token_time:
        first_token_time = time.time() - start

print(f"Time to first token: {first_token_time:.3f}s")
# Look up per-request latency and throughput after the fact, using the
# generation ID from the response (or the final streamed chunk).
curl "https://bazaarlink.ai/api/v1/generation?id=chatcmpl-abc123" \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY"

# Response
{
  "data": {
    "id": "chatcmpl-abc123",
    "model": "google/gemini-2.5-flash",
    "duration_ms": 842,
    "throughput": 61.2,
    "usage": { "prompt_tokens": 12, "completion_tokens": 48, "total_tokens": 60 }
  }
}
# Use provider.sort for automatic latency optimization
response = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[{"role": "user", "content": "Hello!"}],
    extra_body={
        "provider": {
            "sort": "latency",  # Always pick lowest-latency provider
        }
    },
)

Uptime Optimization

BazaarLink maximizes API availability through multiple layers: automatic failover, circuit breakers, and provider health monitoring.

Note
BazaarLink tracks availability for all upstream providers. When a provider's error rate exceeds a threshold, the circuit breaker automatically triggers and routes requests to the next available provider.

Availability Mechanisms

  • Circuit breaker: auto-detects and isolates failing providers
  • Automatic failover: seamlessly switches to a backup provider — no code changes needed
  • Provider health monitoring: continuously tracks error rates and latency per provider
  • Retry logic: transient errors (5xx) are automatically retried

Circuit Breaker

# BazaarLink handles failover automatically — no code changes needed.
# Configure fallback models for maximum resilience:

response = client.chat.completions.create(
    model="openai/gpt-4o",       # Primary model
    messages=[{"role": "user", "content": "Hello!"}],
    extra_body={
        "models": [              # Fallback chain
            "openai/gpt-4o",
            "anthropic/claude-sonnet-4.6",
            "google/gemini-2.5-flash",
        ],
        "route": "fallback",     # Enable fallback routing
    },
)

# Check if failover was used (in usage logs)
# "is_failover": true indicates the primary provider was bypassed
Provider health monitoring is an internal ops view
GET /api/admin/provider-health is an internal endpoint for the ops dashboard, gated by admin auth. It returns a full operational payload (per-provider request volume, error rates, latency percentiles, failover stats, and more) — it's not a public customer API, so we don't reproduce the exact fields here.

Guardrails

Add content-safety mechanisms to your API requests to filter harmful content and enforce compliance policies. BazaarLink currently offers customizable content-filter guardrails only at the organization level; personal (non-org) API keys have no equivalent setting — content safety relies entirely on each upstream model provider's own built-in safety systems.

Current Scope
Personal API keys have no built-in custom guardrails — content safety relies entirely on the upstream provider's own safety systems. If you need customizable content-filter rules (block/redact/flag, keyword and regex rules, built-in PII templates), create an organization and use an organization API key — configured under "Content Filter Guardrails."

Planned Features (not yet available for either personal or org keys)

Guardrail
Description
PII detectionDetect and redact personally identifiable information
Topic restrictionRestrict model responses to approved topics only
Output validationValidate model outputs against custom rules before returning

Current Behavior

Personal API keys: all upstream providers have their own content safety systems — model responses that trigger content filters return with finish_reason: "content_filter", and BazaarLink applies no additional filtering. Organization API keys: org_admin can configure custom rules (block/redact/flag) under "Content Filter Guardrails," applied before text reaches the model.

Cursor IDE Integration

Use BazaarLink as Cursor's OpenAI Override URL. Drop-in setup with auto-conversion of Responses API bodies, tool-format normalization, and the bz- prefix convention for Claude models.

Quick setup

In Cursor, open Settings → Models, then:

  1. Set Override OpenAI Base URL to https://bazaarlink.ai/v1
  2. Set Override OpenAI API Key to your sk-bl-... BazaarLink key
  3. Add the model name you want — see below for Claude (the bz- prefix).
Backward compatibility
The legacy URL https://bazaarlink.ai/v1/cursor still works — it's now a thin re-export of /v1/chat/completions. New setups should use /v1 directly.

The bz- prefix (for Claude models)

Cursor's client-side validation reroutes any model name starting with claude- through Cursor's own Anthropic integration, bypassing your Override URL. To force Cursor to send the request to BazaarLink, prefix the model name with bz-. The server strips the prefix and resolves the rest via the alias map.

Type in CursorResolves to
bz-claude-sonnet-4.6anthropic/claude-sonnet-4.6
bz-claude-opus-4.7anthropic/claude-opus-4.7
gpt-4oopenai/gpt-4o
gemini-2.5-flashgoogle/gemini-2.5-flash

The dot-vs-hyphen variants are normalized: bz-claude-sonnet-4.6 and bz-claude-sonnet-4-6 both resolve to the same model.

CURSOR_MODEL_MAP env var (operator override)

For self-hosted BazaarLink deployments, set this env var to remap arbitrary Cursor-side model names to catalog canonical ids:

CURSOR_MODEL_MAP=gpt-claude-sonnet:anthropic/claude-sonnet-4.6,gpt-opus:anthropic/claude-opus-4.7

Now gpt-claude-sonnet typed in Cursor maps to anthropic/claude-sonnet-4.6 server-side. Useful when you want Cursor to think a model is GPT-family (so it routes through the Override URL) while you actually serve Claude.

What happens automatically

When a request hits /api/v1/chat/completions, BazaarLink applies these compatibility transforms transparently — you don't need to do anything client-side:

  • Auto-detects Responses API bodies — if the body has input instead of messages, it's converted to Chat Completions shape (Cursor sends Responses API format for GPT-family models).
  • Wraps flat tool definitions — Cursor Agent sends { name, description, parameters } without a function wrapper. We wrap it so Anthropic doesn't reject as Tool '' not found in provided tools.
  • Coerces malformed tool_choice — Cursor sends { type: "auto" } (object form, no function). The OpenAI spec requires the string form for auto/none/required, so we coerce.
  • Strips OpenAI-only fields when routing to non-OpenAI providers — parallel_tool_calls, logprobs, top_logprobs, logit_bias, service_tier, user are removed before forwarding (Anthropic returns 400 otherwise).
  • Maps max_output_tokens → max_tokens and strips Responses-API-only fields (previous_response_id, truncation, background, store). The reasoning field is preserved for Chat-Completions-native bodies.

Cursor Agent mode

Tool calling works through the standard Chat Completions tool-call flow. Cursor sends tools (Shell, Read, Write, Grep, etc.) with tool_choice: "auto"; BazaarLink forwards to your chosen provider, which decides whether to call a tool. Tool calls return as standard OpenAI tool_calls deltas; Cursor executes locally and continues the conversation. Works the same regardless of whether you pick gpt-4o (native OpenAI) or bz-claude-sonnet-4.6.

Debugging upstream rejects
If you see provider 4xx errors, check the admin Provider Health panel. Every 4xx response is persisted with the full upstream error body and a summary of the request body we forwarded — click any 🔴 row to expand the JSON.

Model Routing

BazaarLink uses the provider/model-name format to route requests to the correct upstream provider. This gives you access to all major models through a single API endpoint.

Model ID Format

{provider}/{model-name}

# Examples
openai/gpt-5.4-mini
anthropic/claude-sonnet-4.6
google/gemini-3-flash-preview
deepseek/deepseek-v3.2

Routing Priority

When you send a request, BazaarLink resolves the upstream provider in this order:

  1. Exact match — looks for a model route matching the full model ID
  2. Provider wildcard — falls back to provider/* routes (e.g. openai/*)
  3. Global wildcard — falls back to * wildcard routes
  4. Default provider key — for a known catalog model only, uses enabled keys marked as default

Browse all available models on the Models page.

Auto Router

Auto Router v3 scores the request into one of 14 task tiers, then uses the current primary and fallback chain configured for that tier. The paid and free tables are managed separately in the admin console.

  • auto — paid routing table. The successful resolved model is billed at its published price.
  • auto:free — free routing table. Calls within the free quota cost $0; after the quota, funded accounts may switch to paid auto routing unless paid fallback is disabled.

How to Use

Set the model to "auto" (paid) or "auto:free" (free) to enable automatic routing:

curl https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $BAZAARLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"auto","messages":[{"role":"user","content":"Review this TypeScript function"}]}'

How v3 selects a tier

General tiers are simple, standard, complex and reasoning. Specialized tiers are coding, vision, image, video, data, search, social, email, calendar and trading. Low-confidence boundary results are promoted one level instead of guessing downward.

  • Tier scoring: messages, tools, length, keywords and structural signals select one of 14 tiers
  • Hard overrides: vision, formal reasoning and specialized tasks can select a tier directly
  • Route lookup: the selected mode reads that tier's current primary and up to five fallbacks; a disabled tier returns 503
  • Execution: BazaarLink tries the primary, then the configured fallback chain in order
  • Response tracking: the resolved model is returned in the response body and X-Auto-Resolved-Model header

Current model tables

The tables below are read from the same live configuration used by inference and the admin console. Administrators can change each tier's primary model, fallback order and enabled state without a deployment.

auto

Tier
Primary
Fallbacks
State
simpleopenai/gpt-5.4-nano
google/gemini-3.1-flash-lite-previewanthropic/claude-haiku-4.5
enabled
standardgoogle/gemini-3-flash-preview
openai/gpt-5.4-minianthropic/claude-haiku-4.5
enabled
complexgoogle/gemini-3.1-pro-preview
anthropic/claude-sonnet-4.6openai/gpt-5.4-pro
enabled
reasoninganthropic/claude-opus-4.7
openai/gpt-5.4-progoogle/gemini-3.1-pro-preview
enabled
codingopenai/gpt-5.3-codex
anthropic/claude-sonnet-4.6openai/gpt-5.4-pro
enabled
visionopenai/gpt-5.4-image-2
enabled
imageopenai/gpt-5.4-image-2
enabled
videobytedance/seedance-2.0-fast
bytedance/seedance-2.0anthropic/claude-sonnet-4.6
enabled
dataopenai/gpt-5.4-pro
anthropic/claude-sonnet-4.6google/gemini-3.1-pro-preview
enabled
searchperplexity/sonar-pro
perplexity/sonar-reasoning-proopenai/gpt-5.4-pro
enabled
socialopenai/gpt-5.4-nano
google/gemini-3.1-flash-lite-previewanthropic/claude-haiku-4.5
enabled
emailopenai/gpt-5.4-nano
google/gemini-3.1-flash-lite-previewanthropic/claude-sonnet-4.6
enabled
calendaropenai/gpt-5.4-nano
google/gemini-3.1-flash-lite-preview
enabled
tradinganthropic/claude-opus-4.7
openai/gpt-5.4-progoogle/gemini-3.1-pro-preview
enabled

auto:free

Tier
Primary
Fallbacks
State
simpledeepseek/deepseek-v4-flash
enabled
standarddeepseek/deepseek-v4-flash
enabled
complexminimax/minimax-m2.5
enabled
reasoningminimax/minimax-m2.5
enabled
codingdeepseek/deepseek-v4-flash
enabled
visionopenai/gpt-5.4-image-2
disabled
imageopenai/gpt-5.4-image-2
disabled
videogoogle/gemini-2.5-flash-lite
disabled
datadeepseek/deepseek-v4-flash
enabled
searchminimax/minimax-m2.5
enabled
socialdeepseek/deepseek-v4-flash
enabled
emaildeepseek/deepseek-v4-flash
enabled
calendardeepseek/deepseek-v4-flash
enabled
tradingdeepseek/deepseek-v4-flash
enabled

Selected models offer a rate-limited free tier. Free eligibility is granted per model by the platform — call the model by its regular ID; the :free suffix is an optional alias (adding it to a paid model does not make it free).

After the free quota runs out
Once the quota is spent, requests continue automatically at the model's paid price as long as your account has balance — no service interruption, billed exactly like any paid call. If you would rather fail than be billed, send the X-Free-Fallback: false header or disable automatic fallback in the key settings; you then get a 429 instead. With no balance, over-quota requests always return 429.
X-Auto-Resolved-Model
The actual selected model is returned in the X-Auto-Resolved-Model response header and in the response body's model field.

Model Variants

Append a suffix to any model ID to change routing behaviour. BazaarLink supports 7 variant types.

Variant Types
There are two categories of variants: Independent Model IDs (the suffixed model is a distinct endpoint) and Routing Shortcuts (the suffix changes how BazaarLink selects a provider without altering the model itself).

Independent Model IDs

These variants exist as separate models with their own pricing and capabilities. BazaarLink tries the full model ID (with suffix) first, then falls back to the base model.

:free
:extended
:thinking
:exacto

Routing Shortcuts

These suffixes modify provider selection without changing the model identity. The suffix is stripped before matching routes.

:floor   # lowest listed input price first
:nitro   # throughput-oriented shortcut
:online  # enable web-search routing

Multi-Provider Behaviour

For upstreams that support variants, suffixes are passed through as-is. For direct providers (e.g., direct OpenAI, Fireworks), the suffix is stripped and BazaarLink handles routing locally.

Free Models

Selected models offer a rate-limited free tier. Free eligibility is granted per model by the platform — call the model by its regular ID; the :free suffix is an optional alias (adding it to a paid model does not make it free).

  • Call the regular model ID (e.g. deepseek/deepseek-v4-flash). Requests are automatically served free of charge while within the free quota.
  • Free usage is limited per user by requests-per-minute and a daily cap. Limits scale with account tier (no credit / credited).
  • When you exceed the free quota and have credits, requests automatically continue on the paid tier at the listed price. Send X-Free-Fallback: false to disable automatic fallback and receive 429 instead. Without credits, over-quota requests return 429.
  • GET /api/v1/models lists a :free entry for every model with a free tier; auto:free always routes to a free model.

Models with a free tier right now

Call these model IDs directly to use the free quota. The list changes as models are added or retired — query the API for the live set.

deepseek/deepseek-v4-flash

Free quota limits

Item
Value
Requests per minute (RPM)10 / min
Daily request budget150 / day
Account tier multiplier — no credit× 1
Account tier multiplier — funded× 3

Your daily budget = the daily request budget above × your account tier multiplier, counted separately per free model. auto:free additionally enforces a parallel per-IP cap. Individual models may carry tighter or looser limits set by the platform — the model page's "Free tier" block always shows the effective values.

After the free quota runs out

Once the quota is spent, requests continue automatically at the model's paid price as long as your account has balance — no service interruption, billed exactly like any paid call. If you would rather fail than be billed, send the X-Free-Fallback: false header or disable automatic fallback in the key settings; you then get a 429 instead. With no balance, over-quota requests always return 429.

# Return 429 instead of switching to paid routing
-H "X-Free-Fallback: false"

Organization Management

When several people share BazaarLink, create an organization to group members into teams and manage API keys, allowed models, budgets, and billing in one place. The organization owns the shared balance, while teams and members can have their own monthly limits.

Manage your organization
To add teams, invite members, or change organization settings, open Settings and choose an organization

Three-Tier Budget System

Every API request checks member, team, and organization limits. Monthly budget or balance failures return HTTP 402; a spend emergency brake returns HTTP 429.

  1. Member monthly budget (OrgMember.monthlyBudget)
  2. Team monthly budget (Team.monthlyBudget)
  3. Org credits balance (Organization.credits)

Usage Reports

Open an organization and select Reports to view monthly costs in four ways:

  • Overview: total spend, margin rate, daily trend chart
  • By Team: per-team spend, share %, model breakdown, budget utilization
  • By Model: per-model spend, avg price ($/1M tokens)
  • By Member: per-member spend — org_admin only

All views support CSV export with BOM prefix for direct Excel compatibility.

Create & Manage Organizations

  1. Open Settings and create an organization in the Organizations section
  2. Select the organization name to open its management area
  3. Create teams and optionally assign cost-center codes and monthly budgets
  4. Invite members by email, assign a role and Team
  5. Issue API keys for members — usage is automatically tagged to the correct Team / member
  6. View the Reports page for monthly spend broken down by Team, Model, or Member

Member Roles

org_adminOrganization administrator. Manages all teams, members, API keys, allowed models, budgets, billing, reports, settings, and emergency brakes.
billing_viewerFinance viewer. Can view the overview, billing, API-key list, and reports, but cannot change settings or view per-member spend.
team_adminTeam administrator. Manages only their own team's members, invites, API keys, budgets, and emergency brakes.
memberRegular member. Uses assigned organization API keys and is subject to member, team, organization, and model limits.

What else can an organization manage?

Beyond members and teams, the organization management area provides:

  • API keys: create keys for an organization, team, or member and restrict their allowed models
  • Content filtering: block, redact, or log sensitive text before it reaches a model
  • Allowed Models: restrict models for the organization, a team, a member, or an individual API key
  • Budgets and emergency brakes: set monthly limits and minute/hour spend protection
  • Reports and billing: review spend, model usage, team allocation, balance, credit limits, and payments
  • Change and security logs: track settings changes, content-filter hits, and security events
  • Institution plans: education organizations can additionally manage student sessions and quotas

Content filtering

These are organization-owned rules that inspect text before it is sent to a model through an organization API key. An org_admin can enable, edit, and test rules under Settings → Content filtering.

  • block: reject the whole request with HTTP 403 before it reaches the model
  • redact: replace matched text with [REDACTED], then send the cleaned text
  • flag: send the request unchanged and record the match in the organization audit log
  • Use built-in sensitive-data and prompt-injection templates, or create keyword and regular-expression rules
  • Up to 100 rules; regular expressions are safety-checked and rules can be previewed with test text
Currently limited to text input
Filtering currently covers Chat Completions text, string input on Responses, and text content on Messages. Images, audio, video, some structured or multimodal content, and model output are not inspected. Do not treat it as complete data-loss prevention or output moderation.

Management API (v1)

Use the Management API to list organizations, create teams, add members, or update budgets without clicking through the web UI. `/api/v1/orgs` handles organization and member management; cost reports use the separate `/api/orgs/:orgId/reports/*` paths below.

Authentication
GET /api/v1/orgs lists organizations belonging to the key owner and accepts a valid API key or signed-in session. Requests that read or change a specific organization, team, or member require a Bearer management key owned by an org_admin, or that org_admin's signed-in session. Create a management key from the organization's API Keys page.

Organizations

GET/api/v1/orgs

List all organizations the caller belongs to, with role and joinedAt.

GET/api/v1/orgs/:orgId

Get org detail including team and member counts.

curl https://bazaarlink.ai/api/v1/orgs \
  -H "Authorization: Bearer sk-bl-YOUR_MANAGEMENT_KEY"

Teams

GET/api/v1/orgs/:orgId/teams

List teams with member counts, ordered by name.

POST/api/v1/orgs/:orgId/teams
namerequired
string
Team display name (must be unique within the org)
costCenterCode
string
Accounting cost center code
monthlyBudget
number | null
Team monthly spend cap in USD
PATCH/api/v1/orgs/:orgId/teams/:teamId

Partial update — include only the fields to change.

DELETE/api/v1/orgs/:orgId/teams/:teamId
# Create a team
curl https://bazaarlink.ai/api/v1/orgs/{orgId}/teams \
  -X POST \
  -H "Authorization: Bearer sk-bl-YOUR_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Engineering", "costCenterCode": "ENG-001", "monthlyBudget": 500}'

Members

GET/api/v1/orgs/:orgId/members

List all members with nested user (id/name/email) and team info.

POST/api/v1/orgs/:orgId/members
emailrequired
string
Email of an existing BazaarLink user
role
string
org_admin | billing_viewer | team_admin | member (default: member)
teamId
string
Assign to a team (required if role is team_admin)
monthlyBudget
number | null
Per-member monthly spend cap in USD

404 if the email address has no BazaarLink account. 409 if already a member. Default role: member.

PATCH/api/v1/orgs/:orgId/members/:memberId

Partial update of role, teamId, or monthlyBudget.

DELETE/api/v1/orgs/:orgId/members/:memberId

Returns 400 if the target is the last org_admin.

# Add a member
curl https://bazaarlink.ai/api/v1/orgs/{orgId}/members \
  -X POST \
  -H "Authorization: Bearer sk-bl-YOUR_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@example.com", "role": "member", "monthlyBudget": 50}'

# Remove a member
curl https://bazaarlink.ai/api/v1/orgs/{orgId}/members/{memberId} \
  -X DELETE \
  -H "Authorization: Bearer sk-bl-YOUR_MANAGEMENT_KEY"

Reports API

The Reports API powers the organization Reports page and can also feed internal dashboards, monthly reconciliation, or automated CSV downloads. org_admin can access every report; billing_viewer can access overview, team, and model reports, but not per-member spend. It accepts a signed-in session or Bearer management key.

Pass both year and month (1–12). overview and by-team default to the current month when omitted, while by-model, by-member, and export require both values. Supplying both every time avoids endpoint-specific surprises.

Endpoint
Description
GET /api/orgs/:orgId/reports/overviewTotal spend, margin rate, daily trend
GET /api/orgs/:orgId/reports/by-teamPer-team spend, share %, model breakdown, budget utilization
GET /api/orgs/:orgId/reports/by-modelPer-model spend, avg price ($/1M tokens)
GET /api/orgs/:orgId/reports/by-memberPer-member spend — org_admin only
GET /api/orgs/:orgId/reports/exportCSV download; add ?view=overview|by-team|by-model|by-member
# Monthly overview via management key
curl "https://bazaarlink.ai/api/orgs/{orgId}/reports/overview?year=2026&month=3" \
  -H "Authorization: Bearer sk-bl-YOUR_MANAGEMENT_KEY"

# By-team breakdown
curl "https://bazaarlink.ai/api/orgs/{orgId}/reports/by-team?year=2026&month=3" \
  -H "Authorization: Bearer sk-bl-YOUR_MANAGEMENT_KEY"

# Export CSV (downloads file)
curl "https://bazaarlink.ai/api/orgs/{orgId}/reports/export?year=2026&month=3&view=by-team" \
  -H "Authorization: Bearer sk-bl-YOUR_MANAGEMENT_KEY" \
  -o report.csv

Error response reference

401API key invalid or revoked
403RBAC denial (insufficient role) or model not in Allowed Models whitelist
402Monthly budget, credit limit, or available balance is insufficient
429Rate limit or scoped spend emergency brake; follow Retry-After when present
503Platform-wide spend emergency brake or temporary service outage

Allowed Models (Whitelist)

Restrict which models your organization, teams, or individual members can call. Useful for blocking expensive or unvetted models, enforcing model standards, or scoping a team to a single provider.

How it works

  • Three independent layers — Organization, Team, Member — each holds its own list (String[] in the database).
  • When all three layers are empty, every model is allowed (default behavior).
  • When one or more layers are non-empty, the effective list is the intersection of the non-empty layers — a model must be allowed at every restricted layer to pass.
  • Changes take effect within a few seconds (60s in-memory + 5min Redis cache; both are cleared on update).

Pattern format

  • Exact match — e.g. openai/gpt-4o (this exact model only).
  • Provider wildcard — e.g. openai/* (any model under the openai/ prefix).
  • Lowercase only. Max 200 entries per list, 100 chars per entry.

Where to manage

Org Portal → Allowed Models. org_admin can edit org / team / member lists; team_admin can edit their own team and members within it.

Error response when blocked

Calls to a disallowed model return HTTP 403 with this body:

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": {
    "message": "Model is not allowed for this account",
    "code": "model_not_allowed"
  }
}

Management API

All endpoints accept Web Session or Bearer Management Key (sk-bl-...). PATCH replaces the entire list; pass [] to clear.

# Org-level list
GET    /api/orgs/:orgId/allowed-models
PATCH  /api/orgs/:orgId/allowed-models

# Team-level list
GET    /api/orgs/:orgId/teams/:teamId/allowed-models
PATCH  /api/orgs/:orgId/teams/:teamId/allowed-models

# Member-level list
GET    /api/orgs/:orgId/members/:memberId/allowed-models
PATCH  /api/orgs/:orgId/members/:memberId/allowed-models

# Example: restrict an org to OpenAI + a specific Anthropic model
curl -X PATCH https://bazaarlink.ai/api/orgs/$ORG_ID/allowed-models \
  -H "Authorization: Bearer sk-bl-..." \
  -H "Content-Type: application/json" \
  -d '{"allowedModels": ["openai/*", "anthropic/claude-sonnet-4.6"]}'

Circuit Breaker (Spend Kill Switch)

A dual-window spend cap that blocks further requests when upstream cost spikes. Designed to contain runaway scripts, infinite loops, or stolen-key abuse before they cost real money.

How it works

  • Two fixed windows are tracked in Redis per scope: 1-minute and 1-hour upstream cost (USD).
  • If either window's spend reaches its threshold, all subsequent requests in that scope are rejected until the window resets.
  • Defaults: $5 / minute, $20 / hour, enabled by default.
  • Counters live in Redis with TTL — recovery is automatic, no manual reset needed for org/team/member trips.

Scopes (member overrides team overrides org)

Each layer can set its own thresholds. Resolution order is member → team → org → platform default — the first non-null value wins per field (cbEnabled, cbMinuteUsd, cbHourlyUsd).

  • Org level — applies to all keys under the organization. Set in Org Portal → Circuit Breaker.
  • Team level — applies to all keys tagged to that team. Overrides org for those keys.
  • Member level — applies only to keys tagged to that member. Overrides team and org.

Trip behavior

When tripped, requests fail fast (no upstream call is made). The response is HTTP 429 with this body:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json

{
  "error": {
    "message": "Spend circuit breaker tripped at member scope (minute window: $5.2341 ≥ $5.00). Try again later or contact your organization owner."
  }
}
Global vs scoped
A separate global platform-wide circuit breaker (operator-controlled, not visible in the org portal) returns HTTP 503 with a Retry-After header. Operators set it to defend the platform against multi-tenant abuse — it cannot be overridden from your org settings.

Audit log

Every trip event and every configuration change is recorded:

  • Trip events — actions org.cb.tripped / team.cb.tripped / org_member.cb.tripped. Deduplicated to one entry per scope+window per hour, so a sustained trip doesn't flood the log.
  • Config changes — actions org.cb.update / team.cb.update / org_member.cb.update. Capture before/after values plus the actor.

Management API

Org admins can read and update settings via API. All endpoints accept Web Session or Bearer Management Key (sk-bl-...). Send any subset of fields in the PATCH body; null clears a field and falls back to the parent layer.

# Org-level config
GET    /api/orgs/:orgId/circuit-breaker
PATCH  /api/orgs/:orgId/circuit-breaker

# Team-level config
GET    /api/orgs/:orgId/teams/:teamId/circuit-breaker
PATCH  /api/orgs/:orgId/teams/:teamId/circuit-breaker

# Member-level config
GET    /api/orgs/:orgId/members/:memberId/circuit-breaker
PATCH  /api/orgs/:orgId/members/:memberId/circuit-breaker

# Example: tighten the org-level cap to $2/min, $10/hr
curl -X PATCH https://bazaarlink.ai/api/orgs/$ORG_ID/circuit-breaker \
  -H "Authorization: Bearer sk-bl-..." \
  -H "Content-Type: application/json" \
  -d '{"cbMinuteUsd": 2, "cbHourlyUsd": 10, "cbEnabled": true}'

# GET response (org scope)
{
  "settings":         { "cbEnabled": true, "cbMinuteUsd": 2,  "cbHourlyUsd": 10  },
  "resolvedSettings": { "cbEnabled": true, "cbMinuteUsd": 2,  "cbHourlyUsd": 10  },
  "liveSpend":        { "minuteSpend": 0.4123, "hourSpend": 3.8721 }
}

API Key Rotation

Regularly rotating API keys is a security best practice. BazaarLink supports zero-downtime key rotation — create a new key first, then migrate, then revoke the old one.

Note
API keys can be revoked at any time from the dashboard or via the management API. Revocation is immediate — all requests using that key will fail instantly.

Rotation Steps

  1. Create a new API key
  2. Update your application or environment variables to use the new key
  3. Verify the new key is working correctly
  4. Disable or delete the old key
# Key CRUD via Bearer auth requires a MANAGEMENT key (keyType: "management").
# Standard keys get 403 on /api/v1/keys — create a management key first,
# or rotate keys from the dashboard UI instead.

# Step 1: Create new key (management key auth)
POST https://bazaarlink.ai/api/v1/keys
Authorization: Bearer $BL_MANAGEMENT_KEY
{"name": "Production v2"}
# → saves new key: sk-bl-NEW_KEY_VALUE

# Step 2: Update your application
# export BAZAARLINK_API_KEY=sk-bl-NEW_KEY_VALUE

# Step 3: Verify new key works
curl https://bazaarlink.ai/api/v1/models \
  -H "Authorization: Bearer sk-bl-NEW_KEY_VALUE"

# Step 4: Revoke old key (management key auth again)
DELETE https://bazaarlink.ai/api/v1/keys/:old_key_id
Authorization: Bearer $BL_MANAGEMENT_KEY

Activity Export

Download your complete API usage history as CSV for financial audits, cost analysis, or compliance reporting.

CSV Export

Log in and go to the Logs page. Click the Export CSV button in the top-right corner to download your full history as a CSV file. No API call required.

CSV Columns

Column
Description
dateISO 8601 timestamp (UTC)
modelModel ID (e.g. openai/gpt-4o)
providerUpstream provider name
prompt_tokensInput token count
completion_tokensOutput token count
total_tokensTotal tokens (prompt + completion)
reasoning_tokensReasoning tokens (o-series / thinking models)
cached_tokensPrompt cache hit tokens
cost_usdCost in USD credits
duration_msEnd-to-end latency in milliseconds
finish_reasonstop / length / content_filter / error
statusHTTP status code from upstream
app_nameX-Title header value (app attribution)

JSON Usage API

For programmatic access, query aggregated stats grouped by period, model, or key:

# Query usage data (grouped / aggregated)
GET https://bazaarlink.ai/api/v1/usage
Authorization: Bearer sk-bl-YOUR_KEY

# With period filtering (day | week | month | year)
GET https://bazaarlink.ai/api/v1/usage?period=month

# Response
{
  "period": "month",
  "since": "2025-01-01T00:00:00.000Z",
  "credits": 10.5000,
  "totals": {
    "spend": 0.1812,
    "requests": 309,
    "tokens": 161200,
    "promptTokens": 95000,
    "completionTokens": 66200
  },
  "byModel": [{ "model": "openai/gpt-4o", "spend": 0.0028, "tokens": 1200, "requests": 5 }],
  "byKey":   [{ "keyName": "My Agent", "spend": 0.0028, "tokens": 1200, "requests": 5 }],
  "byApp":   [{ "appName": "MyApp", "spend": 0.0015, "tokens": 600, "requests": 3 }],
  "timeSeries": [{ "date": "2025-01-15", "model": "openai/gpt-4o", "cost": 0.0012, "tokens": 500, "requests": 2 }]
}

Usage Accounting

Query detailed usage statistics via API, including token consumption, cost analysis, and request history.

Note
Usage data is billed in USD. Individual request records are available on the Logs page or via CSV export. Aggregated stats (by period, model, or key) are available via the `/api/v1/usage` endpoint with Bearer token auth.

Response Field Reference

FieldTypeDescription
modelstringModel ID used (e.g., openai/gpt-4o)
providerstringUpstream provider name
prompt_tokensnumberInput tokens consumed
completion_tokensnumberOutput tokens generated
total_tokensnumberTotal tokens (prompt + completion)
reasoning_tokensnumberReasoning tokens (for thinking models)
cached_tokensnumberPrompt tokens served from cache
costnumberTotal cost in USD credits
duration_msnumberEnd-to-end latency in milliseconds
throughputnumberGeneration speed in tokens/sec
finish_reasonstringstop | length | content_filter | error
statusnumberHTTP status code from upstream
app_namestring | nullApplication name (X-Title header)
key_namestringAPI key name used for the request
import httpx

# Aggregated stats (Bearer token — period: day | week | month | year)
response = httpx.get(
    "https://bazaarlink.ai/api/v1/usage",
    headers={"Authorization": "Bearer sk-bl-YOUR_KEY"},
    params={"period": "month"},
)

data = response.json()
totals = data["totals"]
print("This month: US$%.4f  (%d requests)" % (totals["spend"], totals["requests"]))

# Cost breakdown by model
for m in data["byModel"]:
    print("  %s: US$%.4f  (%d reqs, %d tokens)" % (m["model"], m["spend"], m["requests"], m["tokens"]))

Institution Plan

The Institution Plan lets any institution (school, enterprise, conference, government, etc.) issue short-lived session tokens to its members from a single org-level key. Members don't need to create a platform account. The org controls which members can request tokens by email domain (e.g. nthu.edu.tw); all usage is billed to the org's account. This page uses the education scenario as an example — the same mechanism works for any institution that needs short-term, multi-user temporary access.

Who is this for
Institutions (schools, enterprises, conferences, government agencies, etc.) that want to give a group of members access to the AI API without creating individual accounts and without handing out long-lived API keys.

Architecture overview

  • Institution KeyStarts with sk-edu-. Created by an org_admin on the org keys page. Cannot be used directly as a Bearer token to call the API — direct calls return 403.
  • Member Session TokenStarts with edu-sess-. Members obtain it after email verification. Default lifetime is 24 hours; revocable by an org admin.
  • Allowed DomainsThe org configures which email domains (exact match, no suffix bypass) may request a session.
  • Usage attributionAll student requests are billed to the org account. Usage is viewable per session and per email in the org dashboard.

Step 1 — Request Institution Plan activation

Contact BazaarLink sales or support (sales@bazaarlink.ai / support@bazaarlink.ai) and let us know your organization wants the Institution Plan enabled, with the list of allowed email domains (e.g. nthu.edu.tw). We will activate the feature for your organization:

{
  "orgType": "education",
  "eduConfig": {
    "allowedDomains": ["nthu.edu.tw", "student.nthu.edu.tw"],
    "sessionTtlSeconds": 86400,
    "verificationTtlSeconds": 900,
    "maxSessionsPerEmailPerKey": 5
  }
}
Domain matching is exact
nthu.edu.tw matches @nthu.edu.tw only — it will not match @nthu.edu.attacker.com. Subdomains must be listed explicitly (e.g. student.nthu.edu.tw).

Step 2 — Org admin creates an Institution Key

On the org's API Keys page, choose "Education" as the key type when creating a new key (this is the internal codename for the institution key). The system generates an sk-edu-... key and shows it ONCE — save it and distribute it through your official channels to that org's members.

Step 3 — Member requests a verification code

Members can request a verification code in two ways: (a) visit /access and enter the institution key + their institutional email — the page calls the API for them; (b) call the API directly:

POST/api/edu/request-code
curl -X POST https://bazaarlink.ai/api/edu/request-code \
  -H "Content-Type: application/json" \
  -d '{
    "key": "sk-edu-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "email": "alice@nthu.edu.tw"
  }'

# Success (incl. unknown key/email — enumeration defence) → {"ok":true,"sent":true}
# Rate limit / resend cooldown → 429 {"error":"rate_limited"} or {"error":"cooldown"}
# Sends a 6-digit verification code to the email; default 15-minute lifetime
Anti-enumeration
request-code always returns 202 regardless of whether the key exists or the email domain is allowed, preventing attackers from probing which institution keys exist. Failed attempts are recorded in the org audit log.

Step 4 — Member submits the code to exchange for a session token

POST/api/edu/verify
curl -X POST https://bazaarlink.ai/api/edu/verify \
  -H "Content-Type: application/json" \
  -d '{
    "key":   "sk-edu-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "email": "alice@nthu.edu.tw",
    "code":  "646291"
  }'

# Success → 200
{
  "token":     "edu-sess-827d11a1ec67d175cfd4f67f929261f4",
  "expiresAt": "2026-05-04T11:16:00.163Z",
  "organization": { "id": "...", "name": "NTHU AI Lab" }
}

# Wrong code → 400 {"error":"invalid"}
# 5 wrong attempts → 429 {"error":"too_many_attempts"} (code invalidated; re-request)

Step 5 — Use the session token to call the API

Use the edu-sess-... token as a Bearer token against any chat / completions / embeddings endpoint:

curl -X POST https://bazaarlink.ai/api/v1/chat/completions \
  -H "Authorization: Bearer edu-sess-827d11a1ec67d175cfd4f67f929261f4" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-haiku-4.5",
    "messages": [{"role": "user", "content": "Hello"}]
  }'
sk-edu- keys cannot be used directly
Sending sk-edu-... directly as a Bearer token to a chat endpoint returns:
403 — Education keys cannot be used directly. Visit /access to exchange for a session.
This is an intentional reverse gate — it stops the institution from leaking long-lived keys to individual members.

Org dashboard — monitoring and revocation

Education-type orgs get an Education tab in the side nav, providing:

  • SettingsAdjust allowed domains, TTL, max sessions per email per key, and per-session request / token / USD quotas.
  • SessionsList all active / expired / revoked sessions; filter by email; revoke individual sessions.
  • Usage statsPer-session call count, token consumption, and accumulated cost.

Security and limits

ItemDefaultDescription
Session TTL24 hoursSession token lifetime; expired sessions require re-verification.
Verification code TTL15 minutesLifetime of the email verification code.
Verification code length6 digitsStored as an HMAC-SHA256 hash in Redis, never in plaintext.
Guess limit5 attemptsBeyond this the code is invalidated immediately.
request-code cooldown60 secondsMinimum interval between repeated requests for the same (key, email).
Per-IP rate limit10 / 15 minAnti-spam.
Per-key rate limit100 / hourPrevents bulk email blasts.
Max sessions per email5Configurable in eduConfig; prevents one inbox from hoarding tokens.
Revocation propagation≤ 60 secondsL1/L2 cache TTL; after DB revocation it takes up to 60 seconds to propagate to all nodes.

Billing and usage attribution

All requests made via session tokens are billed 100% to the organization that owns the institution key, in line with how upstream providers (OpenAI / Anthropic / etc.) bill (per-token). The org dashboard supports drill-down by session, by email, and by key.

Report Feedback

Help us improve BazaarLink by reporting issues, bugs, or suggestions. We actively monitor all feedback channels.

How to Report

Channel
Best For
Response Time
Contact PageGeneral feedback, feature requests1-2 business days
EmailBug reports, technical issuesWithin 24 hours
API Response HeadersAuto-reported errors and metricsAutomatic

What to Include

  • Request ID (from the response id field)
  • Model used and parameters sent
  • Expected vs actual behavior
  • Timestamps and frequency of issue
  • Error messages or HTTP status codes

Visit our Contact page to submit feedback.

FAQ

How is BazaarLink different from calling OpenAI directly?
BazaarLink provides USD billing with NTD-quoted pricing, unified invoices, Chinese support, and a single API across all major models. You can access OpenAI, Anthropic, Google, and more with the same code.
Do I need to change my existing code?
Just change the base URL and API key. All other settings (except model IDs) remain unchanged.
Does BazaarLink store my messages?
By default, we do not store message content. We only log token counts and timestamps for billing purposes.
How do I get a unified invoice (統一發票)?
Enterprise monthly billing customers receive a Taiwan unified e-invoice (統一發票, including business tax ID) at each billing cycle. Pay-as-you-go top-ups via Stripe currently generate a Stripe receipt only — contact sales to switch to monthly billing if you need a Taiwan e-invoice.
What payment methods are supported?
All major credit cards are accepted (Visa, Mastercard, American Express).
Which OpenAI SDK features are supported?
Chat completions, streaming, tool calling, structured output (response_format), and assistant prefill all work. Features are passed through to the upstream provider.
Can I use BazaarLink with agent frameworks like LangChain or CrewAI?
Yes! Any framework that supports the OpenAI API works with BazaarLink. Just set the base URL and use your BazaarLink API key. See the Agentic Usage section for examples.
Support
Support
Hi! How can we help you?
Send a message and we'll get back to you soon.