{"record":{"id":"d8420451a01def12","repo":"KeygraphHQ/shannon","slug":"shannon-ai-model-must-be-provider-model-id","errorCode":null,"errorMessage":"SHANNON_AI_MODEL must be \"<provider>:<model-id>\", got \"${trimmed}\". Example: ${DEFAULT_MODEL_SPEC}","messagePattern":"SHANNON_AI_MODEL must be \"<provider>:<model-id>\", got \"(.+?)\"\\. Example: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/worker/src/ai/models.ts","lineNumber":114,"sourceCode":"  }\n  return raw;\n}\n\nexport interface ModelSpec {\n  providerId: string;\n  modelId: string;\n}\n\n/**\n * Parse a `<provider>:<model-id>` spec. Splits on the first colon only, so colons\n * inside a model ID survive. The provider id is passed through as given — pi's\n * registry validates it later — so this throws only on a malformed spec.\n */\nexport function parseModelSpec(spec: string): ModelSpec {\n  const trimmed = spec.trim();\n  const separator = trimmed.indexOf(':');\n  if (separator === -1) {\n    throw new Error(\n      `SHANNON_AI_MODEL must be \"<provider>:<model-id>\", got \"${trimmed}\". Example: ${DEFAULT_MODEL_SPEC}`,\n    );\n  }\n\n  const providerId = trimmed.slice(0, separator).trim();\n  const modelId = trimmed.slice(separator + 1).trim();\n\n  if (!providerId || !modelId) {\n    throw new Error(\n      `SHANNON_AI_MODEL must be \"<provider>:<model-id>\", got \"${trimmed}\". Example: ${DEFAULT_MODEL_SPEC}`,\n    );\n  }\n\n  return { providerId, modelId };\n}\n\n/** Resolve the run's model from SHANNON_AI_MODEL, falling back to the default. */\nexport function resolveModelSpec(): ModelSpec {","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/KeygraphHQ/shannon/blob/1ae0a142f8525410a688f0309fd003cc5b1d92de/apps/worker/src/ai/models.ts#L96-L132","documentation":"parseModelSpec rejects a SHANNON_AI_MODEL spec that contains no colon. The spec must follow '<provider>:<model-id>'; the FIRST colon is the separator so colons inside a model id (notably Bedrock ARNs) survive. With no separator the provider half cannot be distinguished from the model half.","triggerScenarios":"SHANNON_AI_MODEL set to a bare model id ('claude-sonnet-4-6'), a bare provider name ('anthropic'), or a slash-delimited id ('anthropic/claude-sonnet-4-6'), then parseModelSpec/resolveModelSpec runs during preflight.","commonSituations":"Copying only the model id from pi.dev/models and dropping the provider prefix; pasting a slash-style id from another tool; an unset $MODEL variable producing 'anthropic:' which still has a colon but see error 2.","solutions":["Use the form '<provider>:<model-id>', e.g. 'anthropic:claude-sonnet-4-6'.","Look up the exact provider:model id at https://pi.dev/models.","Leave SHANNON_AI_MODEL unset to use the default 'anthropic:claude-sonnet-4-6'."],"exampleFix":"# before\nexport SHANNON_AI_MODEL=claude-sonnet-4-6\n\n# after\nexport SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6","handlingStrategy":"validation","validationCode":"const spec = process.env.SHANNON_AI_MODEL;\nif (spec && !spec.includes(':')) {\n  console.error(`SHANNON_AI_MODEL=\"${spec}\" must be \"<provider>:<model-id>\". Example: anthropic:claude-sonnet-4-6`);\n  process.exit(1);\n}","typeGuard":"function isModelSpec(spec: string): boolean {\n  const sep = spec.indexOf(':');\n  if (sep === -1) return false;\n  const provider = spec.slice(0, sep).trim();\n  const model = spec.slice(sep + 1).trim();\n  return provider.length > 0 && model.length > 0;\n}","tryCatchPattern":null,"preventionTips":["Always write SHANNON_AI_MODEL as provider:model, copying both halves from https://pi.dev/models.","Leave the variable unset to use the default 'anthropic:claude-sonnet-4-6' when unsure."],"tags":["config","environment","model-selection"],"backgroundTag":null,"analyzedSha":"1ae0a142f8525410a688f0309fd003cc5b1d92de","analyzedAt":"2026-08-12T17:40:03.583Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}