Skip to content

Chat / LLM ​

FOTOhub provides two chat completion endpoints supporting multiple LLM providers. The OpenAI-compatible endpoint follows the standard OpenAI chat completions format, making it a drop-in replacement for existing integrations. The premium endpoint provides access to premium chat models with token-based billing for precise cost control.

Both endpoints support multi-turn conversations, system prompts, and return detailed billing information in every response. Streaming via Server-Sent Events is available on the OpenAI-compatible endpoint.

OpenAI SDK Compatible

The /v1/ai/chat/completions endpoint is fully compatible with the OpenAI Python and TypeScript SDKs. Simply change the base URL to https://apis.fotohub.app/v1/ai and use your FOTOhub API key — the SDK appends /chat/completions itself, so there is no separate request to the bare base URL. See the OpenAI Compatibility section below for drop-in usage examples.


OpenAI-Compatible Chat Completions ​

POST /v1/ai/chat/completions

Auth: API Key (Bearer token) | Billing: per token

Standard chat completions endpoint compatible with OpenAI SDKs. Supports Google Gemini, OpenAI GPT, and Anthropic Claude models.

Parameters ​

ParameterTypeRequiredDefaultDescription
modelstringNogemini-flashModel ID to use for completion. See pricing table below for available models.
messagesarrayYes--Array of message objects. Each message has a role (system, user, or assistant) and content (string or array for vision). System messages set behavior, user messages are inputs, assistant messages are for multi-turn context.
temperaturenumberNo--Accepted but ignored. See the compatibility note below.
max_tokensintegerNo--Accepted but ignored. See the compatibility note below.
streambooleanNo--Accepted but ignored. This endpoint does not stream; see Streaming.

Only model and messages are read

This endpoint exists for drop-in OpenAI SDK compatibility, so it accepts the full OpenAI request body without erroring — but it only acts on model and messages. temperature, max_tokens, stream, top_p, frequency_penalty and presence_penalty are discarded silently: sending them is harmless, but they will not change the output. If you need sampling control or streaming, use the premium endpoint (/v1/ai/chat/claude) or the agent endpoint (/v1/ai/agent/stream) instead.

Pricing Table ​

Billed on the tokens you actually use, in credits, at the per-1M rates below. The charge is fractional: a short reply costs a fraction of one credit rather than rounding up to a whole one. When your included credits are exhausted the request is billed from your USD wallet instead.

ModelIDCredits/1M inCredits/1M outBest for
Gemini Flashgemini-flash20166.7Fast responses, bulk tasks
Gemini Progemini-pro83667Balanced quality, general use
GPT-4ogpt-4o2001000Multimodal, vision, creative
Claude Sonnetclaude-sonnet2001000Code, analysis, reasoning

Output tokens cost 4-8x input tokens, which is why the two columns are quoted separately -- a single blended number would misprice any workload that is not half prompt and half completion. A 25-token prompt with a 150-token answer on gemini-flash costs 25/1e6 * 20 + 150/1e6 * 166.7 = 0.0255 credits.

The exact amount charged comes back on every response in billing.usd_charged, with billing.basis: "tokens" confirming it was derived from real token counts.

Available Models ​

This endpoint accepts exactly the four IDs in the pricing table above. Any other value returns 400 Unknown chat model listing the supported set -- it is never silently substituted with a default, so a typo fails loudly instead of quietly serving you a different model.

GET /v1/models?category=text returns the same four. The premium endpoint (/v1/ai/chat/claude) serves a different and larger family of models with their own IDs in dot notation (claude-sonnet-4.6); those IDs are not accepted here.

Response ​

json
{
  "id": "chatcmpl-fh-abc123",
  "object": "chat.completion",
  "created": 1719849600,
  "model": "gemini-flash",
  "usd_charged": 0.0255,
  "billing": {
    "method": "wallet",
    "usd_charged": 0.0255,
    "usd_charged": 0,
    "basis": "tokens"
  },
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "The Eiffel Tower is a wrought-iron lattice tower located on the Champ de Mars in Paris, France. It was constructed from 1887 to 1889 as the centerpiece of the 1889 World's Fair."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 25,
    "completion_tokens": 150,
    "total_tokens": 175
  }
}

Premium Chat (Token-Based Billing) ​

POST /v1/ai/chat/claude

Auth: API Key (Bearer token) | Billing: token-based (per-token pricing)

Access to FOTOhub's premium chat models — the Claude-class and Nova-class families — with precise token-based billing and per-token cost breakdowns. Both endpoints bill on real token counts; the difference is that this one prices natively in USD and returns a cost_breakdown, while the OpenAI-compatible endpoint prices in credits.

Parameters ​

ParameterTypeRequiredDefaultDescription
modelstringNoclaude-sonnet-4.6Premium model ID. Must be one of the supported models listed in the pricing table below. Sending an unsupported ID returns 400 Unknown model.
messagesarrayYes--Array of message objects with role (user or assistant) and content (string or array for vision). Note: the system prompt is passed via the separate system parameter, not as a message.
temperaturenumberNo0.7Controls randomness. Range 0-1 for Claude models, 0-2 for Nova models. Lower = more deterministic.
max_tokensintegerNo4096Maximum tokens to generate.
systemstringNo--System prompt that defines the assistant's behavior and persona. Passed separately from messages. Supports multi-paragraph instructions.

Streaming

The premium endpoint returns a single complete response (no SSE streaming). For token-by-token streaming use POST /v1/ai/agent/stream — it serves the same premium models. The OpenAI-compatible endpoint does not stream either, regardless of stream: true.

Premium Models (Token-Based Pricing) ​

All models below are billed natively in USD, per token. The figures below are the base per-1M-token USD rates; the amount charged is computed from your actual input/output token counts plus the platform margin and returned as cost_usd. Because the calculation never leaves USD it does not move with the PLN exchange rate, and this endpoint's billing object carries no PLN field at all.

Claude ​

ModelIDInput ($/1M)Output ($/1M)Features
Claude Sonnet 4.6claude-sonnet-4.6$3.00$15.00Vision, 200K ctx, tool-use (default)
Claude Sonnet 4.5claude-sonnet-4.5$3.00$15.00Vision, 200K ctx
Claude Sonnet 4claude-sonnet-4$3.00$15.00Vision, 200K ctx
Claude Haiku 4.5claude-haiku-4.5$0.80$4.00Vision, 200K ctx, fastest

Nova ​

ModelIDInput ($/1M)Output ($/1M)Features
Nova Premiernova-premier$2.50$10.00Best Nova, complex tasks
Nova Pronova-pro$0.80$3.20Vision, reasoning, balanced
Nova Litenova-lite$0.06$0.24Vision, fast, budget
Nova 2 Litenova-2-lite$0.04$0.16Vision, new generation
Nova Micronova-micro$0.035$0.14Text-only, cheapest

Model IDs use dot notation

Premium model IDs use dots (claude-sonnet-4.6), unlike the credit-based endpoint which uses dashes (claude-sonnet-4-6). Use the exact ID from the table above.

Token-Based Billing

Premium models use token-based billing, meaning costs are calculated per-token after generation completes. The billing.cost_breakdown field in the response provides exact input/output token counts and the resulting USD charge in cost_usd. A minimum of 1 credit-equivalent is deducted per request.

usd_charged is what actually left your wallet: it is 0 while the request is covered by included credits (method: "credits") and equals the operation price once credits run out (method: "wallet"). cost_breakdown.cost_usd is the token cost either way — read that one to attribute spend per request.

Response ​

json
{
  "id": "chatcmpl-fh-xyz789",
  "object": "chat.completion",
  "created": 1719849600,
  "model": "claude-sonnet-4.6",
  "billing": {
    "method": "wallet",
    "usd_charged": 2,
    "usd_charged": 0,
    "cost_breakdown": {
      "input_tokens": 150,
      "output_tokens": 500,
      "total_tokens": 650,
      "cost_usd": 0.011925
    }
  },
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Here is a detailed analysis of the architectural patterns used in your codebase..."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "input_tokens": 150,
    "output_tokens": 500,
    "total_tokens": 650
  }
}

Streaming ​

The chat endpoints do not stream

Neither /v1/ai/chat/completions nor /v1/ai/chat/claude streams. Both return one complete JSON body. stream: true on the OpenAI-compatible endpoint is accepted and then ignored — you will receive an ordinary chat.completion response, not an SSE stream, and there is no chat.completion.chunk object anywhere in this API.

The only streaming endpoint is POST /v1/ai/agent/stream, documented below.

POST /v1/ai/agent/stream ​

The streaming twin of POST /v1/ai/agent. Same request contract, same billing, same single-turn semantics — the caller still drives each round of tool use. The difference is that the assistant turn arrives as SSE frames instead of one JSON body, so a CLI can render text as it is produced.

Models (a subset of the premium catalog — only models supporting tool-use): claude-sonnet-4.6 (default), claude-sonnet-4.5, claude-sonnet-4, claude-haiku-4.5. An unlisted ID returns 400.

ParameterTypeRequiredDescription
messagesarrayYesConversation so far. Empty or missing returns 400.
modelstringNoOne of the four IDs above. Default claude-sonnet-4.6.
toolsarrayNoTool definitions. Must be a list if present, else 400.

Authentication is checked before the stream opens, so an invalid key is a plain 401 rather than an error frame you have to parse.

Frame Format ​

Each frame is data: followed by one JSON object, separated by two newlines. The stream terminates with data: [DONE]. Note this is not the OpenAI chunk format — frames are discriminated by a type field, not by choices[].delta.

typePayload
text_deltatext — the next fragment of assistant text
tool_useid, name, input — emitted once the tool call is fully accumulated, never partially
donestop_reason, usage, billing — always the last frame before [DONE]
errormessage — terminal; [DONE] still follows
data: {"type":"text_delta","text":"Let me check"}

data: {"type":"text_delta","text":" the weather."}

data: {"type":"tool_use","id":"toolu_01A","name":"get_weather","input":{"city":"Warsaw"}}

data: {"type":"done","stop_reason":"tool_use","usage":{"input_tokens":150,"output_tokens":48,"total_tokens":198},"billing":{"method":"credits","usd_charged":2,"usd_charged":0}}

data: [DONE]

Disconnecting early does not avoid the charge

Billing runs exactly once, on completion, from the accumulated token usage. If your client disconnects mid-stream the turn is still billed for what the model produced — those tokens were generated and charged upstream either way. This is the same behaviour as the blocking route.

Streaming Example -- SSE Parsing ​

python
import requests
import json

url = "https://apis.fotohub.app/v1/ai/agent/stream"
headers = {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json"
}
payload = {
    "model": "claude-sonnet-4.6",
    "messages": [{"role": "user", "content": "Explain quantum computing"}],
}

response = requests.post(url, json=payload, headers=headers, stream=True)

full_content = ""
for line in response.iter_lines():
    if not line:
        continue
    line = line.decode("utf-8")
    if not line.startswith("data: "):
        continue
    data = line[6:]  # Remove "data: " prefix
    if data == "[DONE]":
        break

    frame = json.loads(data)
    if frame["type"] == "text_delta":
        full_content += frame["text"]
        print(frame["text"], end="", flush=True)
    elif frame["type"] == "tool_use":
        print(f"\n[tool] {frame['name']}({frame['input']})")
    elif frame["type"] == "done":
        print(f"\nstop_reason={frame['stop_reason']} "
              f"credits={frame['billing']['usd_charged']} "
              f"usd={frame['billing']['usd_charged']}")
    elif frame["type"] == "error":
        raise RuntimeError(frame["message"])
typescript
const response = await fetch("https://apis.fotohub.app/v1/ai/agent/stream", {
  method: "POST",
  headers: {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-sonnet-4.6",
    messages: [{ role: "user", content: "Explain quantum computing" }],
  }),
});

const reader = response.body!.getReader();
const decoder = new TextDecoder();
let fullContent = "";
// Frames are split by a blank line, and a single read() can end mid-frame --
// buffer until a separator is seen rather than parsing each chunk directly.
let buffer = "";

outer: while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  const frames = buffer.split("\n\n");
  buffer = frames.pop() ?? "";

  for (const raw of frames) {
    if (!raw.startsWith("data: ")) continue;
    const data = raw.slice(6).trim();
    if (data === "[DONE]") break outer;

    const frame = JSON.parse(data);
    if (frame.type === "text_delta") {
      fullContent += frame.text;
      process.stdout.write(frame.text);
    } else if (frame.type === "tool_use") {
      console.log(`\n[tool] ${frame.name}`, frame.input);
    } else if (frame.type === "done") {
      console.log(`\nstop_reason=${frame.stop_reason}`, frame.billing);
    } else if (frame.type === "error") {
      throw new Error(frame.message);
    }
  }
}
go
package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
)

func main() {
	payload := map[string]interface{}{
		"model":    "claude-sonnet-4.6",
		"messages": []map[string]string{{"role": "user", "content": "Explain quantum computing"}},
	}
	body, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://apis.fotohub.app/v1/ai/agent/stream", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer fh_live_your_api_key")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	scanner := bufio.NewScanner(resp.Body)
	var fullContent string

	for scanner.Scan() {
		line := scanner.Text()
		if !strings.HasPrefix(line, "data: ") {
			continue
		}
		data := strings.TrimPrefix(line, "data: ")
		if data == "[DONE]" {
			break
		}

		var frame map[string]interface{}
		if err := json.Unmarshal([]byte(data), &frame); err != nil {
			continue
		}

		switch frame["type"] {
		case "text_delta":
			text, _ := frame["text"].(string)
			fullContent += text
			fmt.Print(text)
		case "tool_use":
			fmt.Printf("\n[tool] %v %v\n", frame["name"], frame["input"])
		case "done":
			fmt.Printf("\nstop_reason=%v billing=%v\n", frame["stop_reason"], frame["billing"])
		case "error":
			panic(frame["message"])
		}
	}
	fmt.Println()
	fmt.Printf("Total: %s\n", fullContent)
}
bash
curl -X POST "https://apis.fotohub.app/v1/ai/agent/stream" \
  -H "Authorization: Bearer fh_live_your_api_key" \
  -H "Content-Type: application/json" \
  -N \
  -d '{
    "model": "claude-sonnet-4.6",
    "messages": [
      {"role": "user", "content": "Explain quantum computing"}
    ]
  }'

Token Counting & Billing ​

Tokens are the fundamental unit of text processing for LLMs. A token is approximately 4 characters or 0.75 words in English. Both input (prompt) and output (completion) tokens are counted toward billing.

Token-Based USD Wallet Billing ​

  • Per-token pricing in USD (input and output rated separately at 1:1 provider cost)
  • Billed directly from your prepaid USD wallet balance
  • Billed after the completion, from the token counts the model reports, because they are not knowable before the call
  • A pre-flight check validates that your wallet has available funds before dispatching to the provider
  • Nothing is charged if the provider errors, so there is no refund to wait for
  • Exact cost and remaining balance are returned in the billing object

Estimating Costs

A typical conversational exchange (100-word prompt, 200-word response) uses approximately 75 input tokens + 150 output tokens = 225 total tokens. On gemini-flash that costs ~$0.00003; on claude-sonnet-4.6 it works out to roughly $0.0037.


Code Examples ​

Basic Chat Completion ​

python
import requests

url = "https://apis.fotohub.app/v1/ai/chat/completions"
headers = {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json"
}

response = requests.post(url, json={
    "model": "gemini-flash",
    "messages": [
        {"role": "user", "content": "What is the capital of France?"}
    ],
    "temperature": 0.3,
    "max_tokens": 256
}, headers=headers)

data = response.json()
print(data["choices"][0]["message"]["content"])
print(f"Tokens used: {data['usage']['total_tokens']}")
print(f"Credits used: {data['usd_charged']}")
typescript
const response = await fetch("https://apis.fotohub.app/v1/ai/chat/completions", {
  method: "POST",
  headers: {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "gemini-flash",
    messages: [
      { role: "user", content: "What is the capital of France?" }
    ],
    temperature: 0.3,
    max_tokens: 256,
  }),
});

const data = await response.json();
console.log(data.choices[0].message.content);
console.log(`Tokens used: ${data.usage.total_tokens}`);
console.log(`Credits used: ${data.usd_charged}`);
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	payload := map[string]interface{}{
		"model":       "gemini-flash",
		"messages":    []map[string]string{{"role": "user", "content": "What is the capital of France?"}},
		"temperature": 0.3,
		"max_tokens":  256,
	}
	body, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://apis.fotohub.app/v1/ai/chat/completions", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer fh_live_your_api_key")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	var data map[string]interface{}
	json.Unmarshal(respBody, &data)

	choices := data["choices"].([]interface{})
	message := choices[0].(map[string]interface{})["message"].(map[string]interface{})
	fmt.Println(message["content"])

	usage := data["usage"].(map[string]interface{})
	fmt.Printf("Tokens used: %.0f\n", usage["total_tokens"])
	if b, ok := data["billing"].(map[string]interface{}); ok {
		fmt.Printf("Cost: $%.6f\n", b["cost_usd"])
	}
}
bash
curl -X POST "https://apis.fotohub.app/v1/ai/chat/completions" \
  -H "Authorization: Bearer fh_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-flash",
    "messages": [
      {"role": "user", "content": "What is the capital of France?"}
    ],
    "temperature": 0.3,
    "max_tokens": 256
  }'

Multi-Turn Conversation ​

Build multi-turn conversations by including the full message history in each request. The model uses previous messages as context to maintain coherent dialogue.

python
import requests

url = "https://apis.fotohub.app/v1/ai/chat/completions"
headers = {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json"
}

# Build conversation history
messages = [
    {"role": "system", "content": "You are a helpful coding assistant. Be concise."},
    {"role": "user", "content": "How do I read a file in Python?"},
    {"role": "assistant", "content": "Use open() with a context manager:\n\nwith open('file.txt', 'r') as f:\n    content = f.read()"},
    {"role": "user", "content": "What about reading it line by line?"}
]

response = requests.post(url, json={
    "model": "gemini-pro",
    "messages": messages,
    "temperature": 0.5,
    "max_tokens": 512
}, headers=headers)

data = response.json()
print(data["choices"][0]["message"]["content"])

# Append assistant response for next turn
messages.append(data["choices"][0]["message"])

# Continue the conversation
messages.append({"role": "user", "content": "Now show me how to write to a file"})
response = requests.post(url, json={
    "model": "gemini-pro",
    "messages": messages,
    "temperature": 0.5,
    "max_tokens": 512
}, headers=headers)

data = response.json()
print(data["choices"][0]["message"]["content"])
typescript
interface Message {
  role: "system" | "user" | "assistant";
  content: string;
}

const url = "https://apis.fotohub.app/v1/ai/chat/completions";
const headers = {
  "Authorization": "Bearer fh_live_your_api_key",
  "Content-Type": "application/json",
};

const messages: Message[] = [
  { role: "system", content: "You are a helpful coding assistant. Be concise." },
  { role: "user", content: "How do I read a file in Python?" },
  { role: "assistant", content: "Use open() with a context manager:\n\nwith open('file.txt', 'r') as f:\n    content = f.read()" },
  { role: "user", content: "What about reading it line by line?" },
];

let response = await fetch(url, {
  method: "POST",
  headers,
  body: JSON.stringify({
    model: "gemini-pro",
    messages,
    temperature: 0.5,
    max_tokens: 512,
  }),
});

let data = await response.json();
console.log(data.choices[0].message.content);

// Append for next turn
messages.push(data.choices[0].message);
messages.push({ role: "user", content: "Now show me how to write to a file" });

response = await fetch(url, {
  method: "POST",
  headers,
  body: JSON.stringify({
    model: "gemini-pro",
    messages,
    temperature: 0.5,
    max_tokens: 512,
  }),
});

data = await response.json();
console.log(data.choices[0].message.content);
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type ChatRequest struct {
	Model       string    `json:"model"`
	Messages    []Message `json:"messages"`
	Temperature float64   `json:"temperature"`
	MaxTokens   int       `json:"max_tokens"`
}

func chat(messages []Message) (string, error) {
	payload := ChatRequest{
		Model:       "gemini-pro",
		Messages:    messages,
		Temperature: 0.5,
		MaxTokens:   512,
	}
	body, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://apis.fotohub.app/v1/ai/chat/completions", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer fh_live_your_api_key")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	var data map[string]interface{}
	json.Unmarshal(respBody, &data)

	choices := data["choices"].([]interface{})
	message := choices[0].(map[string]interface{})["message"].(map[string]interface{})
	return message["content"].(string), nil
}

func main() {
	messages := []Message{
		{Role: "system", Content: "You are a helpful coding assistant. Be concise."},
		{Role: "user", Content: "How do I read a file in Python?"},
		{Role: "assistant", Content: "Use open() with a context manager:\n\nwith open('file.txt', 'r') as f:\n    content = f.read()"},
		{Role: "user", Content: "What about reading it line by line?"},
	}

	reply, _ := chat(messages)
	fmt.Println(reply)

	// Continue conversation
	messages = append(messages, Message{Role: "assistant", Content: reply})
	messages = append(messages, Message{Role: "user", Content: "Now show me how to write to a file"})

	reply, _ = chat(messages)
	fmt.Println(reply)
}
bash
curl -X POST "https://apis.fotohub.app/v1/ai/chat/completions" \
  -H "Authorization: Bearer fh_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-pro",
    "messages": [
      {"role": "system", "content": "You are a helpful coding assistant."},
      {"role": "user", "content": "How do I read a file in Python?"},
      {"role": "assistant", "content": "Use open() with a context manager."},
      {"role": "user", "content": "What about reading it line by line?"}
    ],
    "temperature": 0.5,
    "max_tokens": 512
  }'

Premium Usage -- Claude with System Prompt ​

Use the premium endpoint for token-based billing with precise cost control. The system parameter is passed separately from messages.

python
import requests

url = "https://apis.fotohub.app/v1/ai/chat/claude"
headers = {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json"
}

response = requests.post(url, json={
    "model": "claude-sonnet-4.6",
    "system": """You are a senior software architect. Provide detailed,
production-ready advice. Include code examples when relevant.
Consider scalability, maintainability, and security in all recommendations.""",
    "messages": [
        {"role": "user", "content": "Design a rate limiting system for a REST API that handles 10K req/s"}
    ],
    "temperature": 0.4,
    "max_tokens": 4096
}, headers=headers)

data = response.json()
content = data["choices"][0]["message"]["content"]
billing = data["billing"]

print(content)
print(f"\n--- Billing ---")
print(f"Method: {billing['method']}")
print(f"Input tokens: {billing['cost_breakdown']['input_tokens']}")
print(f"Output tokens: {billing['cost_breakdown']['output_tokens']}")
print(f"Token cost: {billing['cost_breakdown']['cost_usd']} USD")
print(f"Wallet charge: {billing['usd_charged']} USD")  # 0 while covered by credits
typescript
const response = await fetch("https://apis.fotohub.app/v1/ai/chat/claude", {
  method: "POST",
  headers: {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-sonnet-4.6",
    system: `You are a senior software architect. Provide detailed,
production-ready advice. Include code examples when relevant.
Consider scalability, maintainability, and security.`,
    messages: [
      { role: "user", content: "Design a rate limiting system for a REST API that handles 10K req/s" }
    ],
    temperature: 0.4,
    max_tokens: 4096,
  }),
});

const data = await response.json();
const content = data.choices[0].message.content;
const billing = data.billing;

console.log(content);
console.log("\n--- Billing ---");
console.log(`Method: ${billing.method}`);
console.log(`Input tokens: ${billing.cost_breakdown.input_tokens}`);
console.log(`Output tokens: ${billing.cost_breakdown.output_tokens}`);
console.log(`Token cost: ${billing.cost_breakdown.cost_usd} USD`);
console.log(`Wallet charge: ${billing.usd_charged} USD`); // 0 while covered by credits
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	payload := map[string]interface{}{
		"model":  "claude-sonnet-4.6",
		"system": "You are a senior software architect. Provide detailed, production-ready advice. Include code examples when relevant. Consider scalability, maintainability, and security.",
		"messages": []map[string]string{
			{"role": "user", "content": "Design a rate limiting system for a REST API that handles 10K req/s"},
		},
		"temperature": 0.4,
		"max_tokens":  4096,
	}
	body, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://apis.fotohub.app/v1/ai/chat/claude", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer fh_live_your_api_key")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	var data map[string]interface{}
	json.Unmarshal(respBody, &data)

	choices := data["choices"].([]interface{})
	message := choices[0].(map[string]interface{})["message"].(map[string]interface{})
	fmt.Println(message["content"])

	billing := data["billing"].(map[string]interface{})
	fmt.Println("\n--- Billing ---")
	fmt.Printf("Method: %s\n", billing["method"])

	breakdown := billing["cost_breakdown"].(map[string]interface{})
	fmt.Printf("Input tokens: %.0f\n", breakdown["input_tokens"])
	fmt.Printf("Output tokens: %.0f\n", breakdown["output_tokens"])
	fmt.Printf("Token cost: %v USD\n", breakdown["cost_usd"])
	fmt.Printf("Wallet charge: %v USD\n", billing["usd_charged"]) // 0 while covered by credits
}
bash
curl -X POST "https://apis.fotohub.app/v1/ai/chat/claude" \
  -H "Authorization: Bearer fh_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4.6",
    "system": "You are a senior software architect. Provide production-ready advice.",
    "messages": [
      {"role": "user", "content": "Design a rate limiting system for a REST API"}
    ],
    "temperature": 0.4,
    "max_tokens": 4096
  }'

Vision -- Analyzing Images ​

Send images to vision-capable models by passing a content array with both text and image URL objects. Supported on Claude, GPT, Nova Lite/Pro, and Gemini models.

python
import requests

url = "https://apis.fotohub.app/v1/ai/chat/completions"
headers = {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json"
}

response = requests.post(url, json={
    "model": "gpt-4o",
    "messages": [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "Describe this image in detail. What objects are visible?"},
                {
                    "type": "image_url",
                    "image_url": {
                        "url": "https://example.com/photo.jpg"
                    }
                }
            ]
        }
    ],
    "max_tokens": 1024
}, headers=headers)

data = response.json()
print(data["choices"][0]["message"]["content"])
print(f"Credits used: {data['usd_charged']}")
typescript
const response = await fetch("https://apis.fotohub.app/v1/ai/chat/completions", {
  method: "POST",
  headers: {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "gpt-4o",
    messages: [
      {
        role: "user",
        content: [
          { type: "text", text: "Describe this image in detail. What objects are visible?" },
          {
            type: "image_url",
            image_url: {
              url: "https://example.com/photo.jpg",
            },
          },
        ],
      },
    ],
    max_tokens: 1024,
  }),
});

const data = await response.json();
console.log(data.choices[0].message.content);
console.log(`Credits used: ${data.usd_charged}`);
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	payload := map[string]interface{}{
		"model": "gpt-4o",
		"messages": []map[string]interface{}{
			{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "text", "text": "Describe this image in detail. What objects are visible?"},
					{
						"type": "image_url",
						"image_url": map[string]string{
							"url": "https://example.com/photo.jpg",
						},
					},
				},
			},
		},
		"max_tokens": 1024,
	}
	body, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://apis.fotohub.app/v1/ai/chat/completions", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer fh_live_your_api_key")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	var data map[string]interface{}
	json.Unmarshal(respBody, &data)

	choices := data["choices"].([]interface{})
	message := choices[0].(map[string]interface{})["message"].(map[string]interface{})
	fmt.Println(message["content"])
	fmt.Printf("Credits used: %.0f\n", data["usd_charged"])
}
bash
curl -X POST "https://apis.fotohub.app/v1/ai/chat/completions" \
  -H "Authorization: Bearer fh_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o",
    "messages": [
      {
        "role": "user",
        "content": [
          {"type": "text", "text": "Describe this image in detail. What objects are visible?"},
          {"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
        ]
      }
    ],
    "max_tokens": 1024
  }'

Vision-Capable Models

The following models support image input. On /v1/ai/chat/completions: gpt-4o, claude-sonnet, gemini-pro, gemini-flash. On the token-billed /v1/ai/chat/claude endpoint: claude-sonnet-4.6, claude-sonnet-4.5, claude-sonnet-4, claude-haiku-4.5, nova-pro, nova-lite, nova-2-lite.

Budget-Friendly with Nova ​

python
import requests

url = "https://apis.fotohub.app/v1/ai/chat/claude"
headers = {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json"
}

# Nova Micro — ultra-budget option for simple tasks
# With margin: $0.0525 per 1M input tokens, $0.21 per 1M output tokens
response = requests.post(url, json={
    "model": "nova-micro",
    "messages": [
        {"role": "user", "content": "Classify this review as positive/negative: 'Great product, fast shipping!'"}
    ],
    "temperature": 0.1,  # Low temp for classification
    "max_tokens": 10
}, headers=headers)

data = response.json()
print(data["choices"][0]["message"]["content"])  # "positive"
print(f"Cost: {data['billing']['cost_breakdown']['cost_usd']} USD")  # ~$0.000002
typescript
// Nova Micro — ultra-budget: $0.0525 per 1M input tokens, $0.21 per 1M output (incl. margin)
const response = await fetch("https://apis.fotohub.app/v1/ai/chat/claude", {
  method: "POST",
  headers: {
    "Authorization": "Bearer fh_live_your_api_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "nova-micro",
    messages: [
      { role: "user", content: "Classify this review as positive/negative: 'Great product, fast shipping!'" }
    ],
    temperature: 0.1,
    max_tokens: 10,
  }),
});

const data = await response.json();
console.log(data.choices[0].message.content); // "positive"
console.log(`Cost: ${data.billing.cost_breakdown.cost_usd} USD`);
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	// Nova Micro — ultra-budget for classification, extraction, simple tasks
	payload := map[string]interface{}{
		"model": "nova-micro",
		"messages": []map[string]string{
			{"role": "user", "content": "Classify this review as positive/negative: 'Great product, fast shipping!'"},
		},
		"temperature": 0.1,
		"max_tokens":  10,
	}
	body, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://apis.fotohub.app/v1/ai/chat/claude", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer fh_live_your_api_key")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	var data map[string]interface{}
	json.Unmarshal(respBody, &data)

	choices := data["choices"].([]interface{})
	message := choices[0].(map[string]interface{})["message"].(map[string]interface{})
	fmt.Println(message["content"]) // "positive"

	billing := data["billing"].(map[string]interface{})
	breakdown := billing["cost_breakdown"].(map[string]interface{})
	fmt.Printf("Cost: %v USD\n", breakdown["cost_usd"])
}
bash
curl -X POST "https://apis.fotohub.app/v1/ai/chat/claude" \
  -H "Authorization: Bearer fh_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nova-micro",
    "messages": [
      {"role": "user", "content": "Classify: positive or negative? Great product!"}
    ],
    "temperature": 0.1,
    "max_tokens": 10
  }'

OpenAI SDK Compatibility ​

The /v1/ai/chat/completions endpoint is a drop-in replacement for the OpenAI API. Use the official OpenAI SDK with your FOTOhub API key and base URL -- no code changes required beyond configuration.

python
from openai import OpenAI

# Point the OpenAI client at FOTOhub
client = OpenAI(
    api_key="fh_live_your_api_key",
    base_url="https://apis.fotohub.app/v1/ai"
)

# Use exactly like the OpenAI API
response = client.chat.completions.create(
    model="gemini-flash",          # Any FOTOhub model ID
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Explain REST APIs in 3 sentences."}
    ],
    temperature=0.7,
    max_tokens=256,
    stream=False
)

print(response.choices[0].message.content)
print(f"Model: {response.model}")
print(f"Tokens: {response.usage.total_tokens}")
typescript
import OpenAI from "openai";

// Point the OpenAI client at FOTOhub
const client = new OpenAI({
  apiKey: "fh_live_your_api_key",
  baseURL: "https://apis.fotohub.app/v1/ai",
});

// Use exactly like the OpenAI API
const response = await client.chat.completions.create({
  model: "gemini-flash",          // Any FOTOhub model ID
  messages: [
    { role: "system", content: "You are a helpful assistant." },
    { role: "user", content: "Explain REST APIs in 3 sentences." },
  ],
  temperature: 0.7,
  max_tokens: 256,
  stream: false,
});

console.log(response.choices[0].message.content);
console.log(`Model: ${response.model}`);
console.log(`Tokens: ${response.usage?.total_tokens}`);
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

// FOTOhub is OpenAI-compatible — use any HTTP client with the same payload format.
// For Go, use github.com/sashabaranov/go-openai or plain HTTP:
func main() {
	payload := map[string]interface{}{
		"model": "gemini-flash",
		"messages": []map[string]string{
			{"role": "system", "content": "You are a helpful assistant."},
			{"role": "user", "content": "Explain REST APIs in 3 sentences."},
		},
		"temperature": 0.7,
		"max_tokens":  256,
	}
	body, _ := json.Marshal(payload)

	// Same endpoint as OpenAI — just change the base URL
	req, _ := http.NewRequest("POST", "https://apis.fotohub.app/v1/ai/chat/completions", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer fh_live_your_api_key")
	req.Header.Set("Content-Type", "application/json")

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	var data map[string]interface{}
	json.Unmarshal(respBody, &data)

	choices := data["choices"].([]interface{})
	message := choices[0].(map[string]interface{})["message"].(map[string]interface{})
	fmt.Println(message["content"])
}
bash
# Same format as OpenAI — just change the URL and API key
curl -X POST "https://apis.fotohub.app/v1/ai/chat/completions" \
  -H "Authorization: Bearer fh_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-flash",
    "messages": [
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "Explain REST APIs in 3 sentences."}
    ],
    "temperature": 0.7,
    "max_tokens": 256
  }'

Migration from OpenAI

To migrate existing OpenAI integrations to FOTOhub:

  1. Change base_url / baseURL to https://apis.fotohub.app/v1/ai
  2. Replace your OpenAI API key with your FOTOhub key (fh_live_...)
  3. Optionally change the model parameter to a FOTOhub model ID (or keep gpt-4o -- it works)

All standard OpenAI parameters are accepted — your existing request bodies will not error. Only model and messages change the result, however: temperature, max_tokens, stream, top_p, frequency_penalty and presence_penalty are ignored. If your integration depends on sampling parameters or on streaming, migrate to /v1/ai/chat/claude or /v1/ai/agent/stream rather than the compatibility shim.


Error Responses ​

400 -- Invalid Request ​

json
{
  "error": {
    "type": "invalid_request",
    "message": "messages array is required and must contain at least one message",
    "code": "missing_messages"
  }
}

402 -- Insufficient Funds ​

json
{
  "error": {
    "type": "billing_error",
    "message": "Insufficient credits. Required: 2, available: 0",
    "code": "insufficient_funds",
    "credits_required": 2,
    "credits_available": 0
  }
}

429 -- Rate Limited ​

json
{
  "error": {
    "type": "rate_limit",
    "message": "Rate limit exceeded. Please retry after 2 seconds.",
    "code": "rate_limited",
    "retry_after": 2
  }
}

Rate Limits ​

Rate Limits by Tier

  • Free tier: 10 requests/minute, 100 requests/day
  • Pro tier: 60 requests/minute, 5,000 requests/day
  • Enterprise: Custom limits -- contact support
  • Streaming requests count as 1 request regardless of duration