Skip to main content

Channels, models and usage tables

Six tables covering model supply and call records (notation per the overview).

channels — upstream provider channels

One channel = one provider account (base_url + key). 9 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)ch_ + 12 hex chars
nameTEXTnochannel name; lower(trim(name)) unique
typeTEXTnoopenai / claude / gemini / mock (schema comment); the admin API validator additionally accepts the aliases anthropic and google (api/admin_handlers.go:194-208)
api_formatTEXTno''valid only for openai channels: chat or responses (Chat Completions vs Responses API); must be empty otherwise
base_urlTEXTno''upstream base URL (empty = official default endpoint)
api_keyTEXTno''provider key stored in plaintext, masked only by the API
enabledINTEGERno1enabled flag
sort_orderINTEGERno0drag ordering
updated_atINTEGER → BIGINTnonow()update time
  • FKs: none outgoing. Incoming: models.channel_id (CASCADE) — the only hard FK into this table; constraint-free soft references also exist (models.fallback_channel_id, usage_logs.channel_id).
  • Index: idx_channels_name_unique (UNIQUE ON lower(trim(name))).
  • Notes: deleting a channel CASCADE-deletes its models. mock appears in the schema comment and in provider code (a mock implementation is used when no API key is set, llm/openai_provider.go:36), but the admin API's type validator rejects the literal mock.

models — model catalog

One callable model per row, carrying full capability, tooling, pricing, and moderation metadata. 34 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)m_ + 12 hex chars
channel_idTEXTnoFK→channels(id), CASCADE
kindTEXTno'chat'chat / image / embedding (api/admin_channel_model_import.go:241)
request_idTEXTnomodel name sent upstream; lower(trim(request_id)) unique within a channel
labelTEXTnodisplay name
descriptionTEXTno''description
iconTEXTno''icon (emoji/URL)
fallback_channel_idTEXTno''backup channel retried when a primary request fails ('' = none)
enabledINTEGERno1listed flag
sort_orderINTEGERno0picker ordering
tool_modeTEXTno'native'tool calling style: native (provider function calling) / prompt (prompt-simulated) / none (tools disabled)
visionINTEGERno1accepts image input
streamINTEGERno1supports SSE streaming
research_enabledINTEGERno1expose Deep Research for this model
fastINTEGERno0THE single fast-mode model platform-wide (invariant enforced by the app, no DB constraint)
system_promptTEXTno''extra system prompt appended for this model
param_controlsTEXT (JSON)no'[]'whitelist of user-tunable parameter controls; each item declares a key (common: temperature, top_p, max_tokens, thinking, effort), a type (toggle/select) and a value→upstream-request-body map (llm/param_controls.go)
extra_paramsTEXT (JSON)no'{}'admin-only upstream request defaults; native provider request fields win (deep-merge)
official_toolsTEXT (JSON)no'[]'provider-hosted tools [{name,icon,request}] (legacy string arrays upgraded during migration)
builtin_toolsTEXT (JSON)yesNULLlocal default tools; NULL = all (backwards compatible), '[]' = none
mcp_server_idsTEXT (JSON)yesNULLdefault attached admin MCP services; NULL/[] = none
tagsTEXT (JSON)no'[]'model_tags ids for picker filtering
moderation_enabledINTEGERno0screen prompts before generation
moderation_modeTEXTno'keyword'keyword (word list) / model (moderation model via the task.moderation call)
price_inputREAL → DOUBLE PRECISIONno0per-million input-token price
price_outputREAL → DOUBLE PRECISIONno0per-million output-token price
price_cache_readREAL → DOUBLE PRECISIONno0cache-read price
price_cache_writeREAL → DOUBLE PRECISIONno0cache-write price
price_per_imageREAL → DOUBLE PRECISIONno0per-image price
currencyTEXTno'USD'pricing currency
dimINTEGERno0embedding output dimension (selects the Qdrant collection aivory_c<dim>)
compaction_token_thresholdINTEGERno0per-model compaction trigger (0 = use the global compaction_token_trigger)
image_timeout_secINTEGERno0image generation timeout in seconds (0 = default)
updated_atINTEGER → BIGINTnonow()update time
  • Indexes: idx_models_channel(channel_id), idx_models_kind(kind, enabled), idx_models_channel_request_unique (UNIQUE ON (channel_id, lower(trim(request_id)))).
  • Incoming references: knowledge_bases.embedding_model_id (no CASCADE — deleting an embedding model still referenced by a KB is rejected), model_group_quotas.model_id, model_skills.model_id (CASCADE); conversations.model_id / messages.model_id are unconstrained snapshots.
  • Notes: python_execute and web search are withheld from a model's tool declarations unless their backends are configured (cmd/api/main.go:131-137) — a behavior of the runtime, not this table, often mistaken for a data issue.

model_tags — model tags

Admin-managed picker filter labels. 4 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)mtag_ + 12 hex chars
nameTEXTnotag name; lower(trim(name)) unique
sort_orderINTEGERno0ordering
created_atINTEGER → BIGINTnonow()creation time
  • Notes: the model side stores tag ids as a JSON array in models.tags, not rows here — deleting a tag does not cascade (the app keeps both sides in sync).

model_skills — model default skills join

Pure join table. 2 columns.

ColumnTypeNullableDefaultNotes
model_idTEXTno (PK part)FK→models(id), CASCADE
skill_idTEXTno (PK part)FK→skills(id), CASCADE
  • PK: composite (model_id, skill_id).

usage_logs — deletable diagnostic detail

One row per upstream call (errors included); prunable from the admin "Usage" page. 25 columns.

ColumnTypeNullableDefaultNotes
idINTEGER → BIGSERIALno (PK)autoincrementthe only autoincrement integer: SQLite INTEGER PRIMARY KEY AUTOINCREMENT (schema.sql:827), PG BIGSERIAL
user_idTEXTnoFK→users(id), CASCADE
conversation_idTEXTyesNULLowning conversation (soft reference, no FK)
message_idTEXTyesNULLowning message (soft reference)
model_idTEXTnomodel that actually served the call (soft snapshot)
purposeTEXTnochat / image / embedding / verify / task.-prefixed internal calls (task.title, task.router, task.rag_evidence_judge, task.rag_map_reduce, task.compact, task.memory_extract, task.memory_adjudicate, task.downgrade, task.research_plan, task.research_verify, task.research_validate, task.moderation, task.search_queries, task.tool_route, task.image_intentllm/task_llm.go:52-99)
input_tokensINTEGER → BIGINTno0input tokens
output_tokensINTEGER → BIGINTno0output tokens
cache_read_tokensINTEGER → BIGINTno0cache read tokens
cache_write_tokensINTEGER → BIGINTno0cache write tokens
images_countINTEGERno0generated image count
costREAL → DOUBLE PRECISIONno0row cost (in the model's currency)
currencyTEXTno'USD'cost currency
creditsREAL → DOUBLE PRECISIONno0credits charged for this row (0 = free/unconverted)
channel_idTEXTno''channel that actually served the request
fallbackINTEGERno01 = served via the model's fallback channel (fallback_channel_id)
statusTEXTno'ok'ok / error (failed requests are logged too)
errorTEXTno''upstream failure detail (admin-only)
request_methodTEXTno''sanitized upstream request scene for status='error' rows
request_urlTEXTno''same
request_headersTEXTno''same (sanitized)
request_bodyTEXTno''same; governed by the admin logging switch log_request_bodies
ttft_fallback_modelTEXTno''non-empty = a TTFT-timeout model-level fallback served this row (orthogonal to channel fallback); value is the fallback model's display name
created_atINTEGER → BIGINTnonow()event time
workspace_idTEXTno''※ ALTER-added: owning workspace (workspace usage pages; '' = personal)
  • Indexes: idx_usage_user_time(user_id, created_at), idx_usage_model_time(model_id, created_at), idx_usage_user_model_time(user_id, model_id, created_at) (authoritative fallback when the cache counter is cold).
  • Notes: this table is a deletable diagnostic copy — analytics never read it (they read usage_stats), so pruning logs can never change a report.

usage_stats — append-only analytics source of truth

Immutable facts for successful calls; the only source behind the dashboards. 19 columns.

ColumnTypeNullableDefaultNotes
source_log_idINTEGER → BIGINTno (PK)matching usage_logs.id; an idempotency key, not a foreign key — with no FK by design, so logs are freely deletable
user_idTEXTyesNULLFK→users(id), SET NULL — the table's only FK: account deletion anonymizes attribution while preserving global/model history
conversation_idTEXTyesNULLsnapshot, no FK
message_idTEXTyesNULLsnapshot, no FK
model_idTEXTnosnapshot, no FK
purposeTEXTnosame taxonomy as usage_logs.purpose
input_tokensINTEGER → BIGINTno0as usage_logs
output_tokensINTEGER → BIGINTno0as usage_logs
cache_read_tokensINTEGER → BIGINTno0as usage_logs
cache_write_tokensINTEGER → BIGINTno0as usage_logs
images_countINTEGERno0as usage_logs
costREAL → DOUBLE PRECISIONno0as usage_logs
currencyTEXTno'USD'as usage_logs
creditsREAL → DOUBLE PRECISIONno0as usage_logs
workspace_idTEXTno''workspace attribution (present in CREATE here, unlike the ALTER-added column on usage_logs)
channel_idTEXTno''serving channel snapshot
fallbackINTEGERno0channel-fallback flag
ttft_fallback_modelTEXTno''model-fallback flag
created_atINTEGER → BIGINTnononecopied from the mirrored log row — deliberately no now() default
  • Mirror mechanism: both dialects install an AFTER INSERT database trigger that mirrors successful rows (rows with status='error' are skipped; ON CONFLICT DO NOTHING keeps it idempotent) — install/backfill in store/usage_stats.go (EnableUsageStatsMirror / BackfillUsageStats, store.go:709-714). The trigger is not preserved by logical dumps (pg_dump); the app reinstalls it at startup.
  • Indexes: idx_usage_stats_time(created_at), (user_id, created_at), (model_id, created_at), (message_id, purpose, source_log_id).