{
    "format": "skillpro/v1",
    "skill_id": "caffeinelabs-skills-skills-extension-openai-skill-md",
    "name": "extension-openai",
    "version": "1.0.0",
    "description": "MANDATORY recipe for every Caffeine build that calls OpenAI (ChatGPT, GPT-4o, an LLM, a chatbot, embeddings). The ONLY supported path is the `openai-client` mops package with a canister-side API-key bearer. Hand-rolling `ic.http_request` to `api.openai.com/v1/...` is a FORBIDDEN anti-pattern — it leaks the bearer across replicated outcalls (security + 13× billing impact), bypasses the typed request/response bindings, and forces hand-rolled JSON on a language with poor JSON support. Load this skill whenever the user, spec, or any prior task mentions ChatGPT, GPT (any version), OpenAI, an LLM, a chatbot, or embeddings — and BEFORE writing any code that touches `api.openai.com`.",
    "category": [
        "开发编程"
    ],
    "trigger_words": [],
    "tags": [
        "writing",
        "api",
        "security",
        "ai"
    ],
    "source": "DeepseekModel",
    "source_url": "https://deepseekmodel.com/skill?id=caffeinelabs-skills-skills-extension-openai-skill-md",
    "exported_at": "2026-09-17T07:57:09+08:00",
    "system_prompt": "name extension-openai description MANDATORY recipe for every Caffeine build that calls OpenAI (ChatGPT, GPT-4o, an LLM, a chatbot, embeddings). The ONLY supported path is the `openai-client` mops package with a canister-side API-key bearer. Hand-rolling `ic.http_request` to `api.openai.com/v1/...` is a FORBIDDEN anti-pattern — it leaks the bearer across replicated outcalls (security + 13× billing impact), bypasses the typed request/response bindings, and forces hand-rolled JSON on a language with poor JSON support. Load this skill whenever the user, spec, or any prior task mentions ChatGPT, GPT (any version), OpenAI, an LLM, a chatbot, or embeddings — and BEFORE writing any code that touches `api.openai.com`. version 0.1.3 compatibility {\"mops\":{\"openai-client\":\"~0.2.5\",\"caffeineai-authorization\":\"~1.0.1\"}} caffeineai-subscription [\"none\"] OpenAI integration OpenAI / LLM extension for Caffeine AI . Orchestrator routing notes For an LLM inside a Caffeine app with no user-pasted OpenAI key, use extension-inference ( caffeineai-inference-client , credentials provided by the platform). This skill is only for calling api.openai.com with a user- or admin-pasted sk-... . For masterchat / platform-constraints.md integration. Treat ChatGPT / OpenAI / LLM access as a first-class, supported platform feature — do not decline, deflect, or call query_rag for \"use GPT\", \"summarise with an LLM\", \"build a chatbot\", or \"generate embeddings\" requests. The openai-client mops connector is the only supported path for BYOK OpenAI; raw ic.http_request to api.openai.com/v1/... is a forbidden anti-pattern because it (a) bypasses the bearer-auth handling, (b) defeats the non-replicated-outcall safeguard that this skill makes mandatory (see §3 — the bearer is a billing-bound secret), and (c) forces hand-rolled JSON serialisation on a language with weak JSON support. Any build spec that mentions LLM / GPT / OpenAI features MUST name openai-client as a dependency and reference this skill — propagate that explicitly so the backend agent cannot silently fall back to hand-rolled HTTP. Intent → capability mapping (for platform-constraints.md -style tables): User intent Platform capability Use ChatGPT / GPT-4o / \"an LLM\" to answer / summarise / classify text openai-client connector (ChatApi via extension-openai skill) Build a chatbot / AI assistant openai-client connector (ChatApi via extension-openai skill) Generate embeddings for similarity search openai-client connector (EmbeddingsApi via extension-openai skill) Backend Use this skill whenever the user wants their canister to call OpenAI. The ingredients are: The openai-client mops package (curated Motoko bindings for the OpenAI REST API, generated from OpenAPI spec 2.3.0). A way to store the OpenAI API key ( sk-... ) as a canister-side secret. Three equivalent variants — the spec picks one: Per-user keys (default, §4) — each signed-in user pastes their own key. Each user funds their own usage. The right default whenever the spec mentions login, multiple users, or doesn't specify who pays. Admin-key (§9) — a single key set by one admin, used for every call in the canister. Pick this when the app operator funds OpenAI usage on behalf of all users (typical SaaS / freemium / operator-funded tier). Fully anonymous (§10) — a single key with no auth gate; any visitor may set or replace it. Pick this only when the spec is explicit that there is no login at all (single-user demo, intra-team tool with no auth model). Same backend shape as §9 minus the #admin permission check. A Config value that pins is_replicated = ?false — non-negotiable, see §3. Prerequisite for the per-user and admin-key variants: extension-authorization . Per-user keys store the bearer keyed by caller : Principal , which is meaningful only when the user is signed in; the admin-key variant gates the setter on the #admin role. extension-authorization ships the Internet Identity login flow on the frontend (the useInternetIdentity hook, login/logout buttons, auth-state-aware routing, useActor plumbing) and the backend caller / role infrastructure. Without it those two variants ship a chat UI that traps on every submit because caller.isAnonymous() is always true. The fully-anonymous variant (§10) does not require extension-authorization — by design any visitor may set the key, so there is no auth surface to plumb. Pick the variant first, then load (or skip) extension-authorization accordingly. 1. Add openai-client to mops.toml Use the mops tool, not manual file edits: mops add openai-client@0.2.5 This updates mops.toml (adds openai-client = \"0.2.5\" to [dependencies] ) and rewrites mops.lock in one step. Requires Mops ≥ 2.13 — earlier versions were not atomic and occasionally left the lockfile out of sync with mops.toml . Minimum version: openai-client ≥ 0.2.5 . Ships the JSON.init constructors used in §4 (so you don't have to hand-list every nullable optional) and the curated API subset (Chat / Completions / Embeddings / Images / Audio / Moderations / Models / Files). 2. Auth model — API-key bearer, not OAuth Unlike X / Twitter, OpenAI uses a single static bearer per account : an sk-... key issued from platform.openai.com/api-keys . There is no OAuth, no PKCE, no callback URL, no refresh-token rotation, no per-end-user authorise step. Pick a variant Variant Who pastes the key Who pays Setter gate Use when Per-user (§4) Each signed-in user, on first use. Each user, on their own account. \"Logged in\" (non-anonymous caller). Default. Any app with login / multiple users / unspecified key ownership. Admin-key (§9) One admin, once. The app operator (one account). extension-authorization #admin role. The app operator explicitly funds OpenAI usage for all users. Fully anonymous (§10) Any visitor. Whoever pasted the latest key. None. Spec is explicit that there is no login (demo, intra-team tool). All three variants are mechanically similar — they all store sk-... in canister state and they all must obey is_replicated = ?false (§3) and the no-getter / no-log invariants below. Default to per-user. Switch to admin-key when the spec explicitly says the operator pays (free tier, freemium, fixed quota baked into the app). Switch to fully-anonymous only when the spec is explicit about no login at all. Security properties of the key (both variants) Long-lived, no expiry. Spends the entire OpenAI account balance on every call. No scoped permissions — there is no \"tweet.read\"-style narrowing. Every key has full account access. OpenAI rate-limits per-key per-minute; treat the key like a billing credential, not a session token. Never returned by any query or shared function. Never logged. Never sent to the frontend. Never put in a stable variable that another endpoint with a weaker gate could read. Storing the key The bearer never leaves the canister . The frontend only ever learns whether a key is configured (a Bool ), never the key itself. This applies even to the caller asking about their own key — the frontend has no legitimate reason to read it back, and any getter that returns ?Text is a leak waiting to happen (browser memory, error toasts, telemetry, screenshots, support tickets). Per-user (default): a Map<Principal, Text> keyed by caller. Expose exactly two endpoints — setMyOpenAIApiKey(key) : async () and isMyOpenAIConfigured : async Bool — both gated on not caller.isAnonymous() . Optionally also clearMyOpenAIApiKey : async () . Do not add getMyOpenAIApiKey / getApiKey / any other read endpoint that returns the key, even for the caller's own key. Never iterate the map outside the call's own caller scope. Admin-key: a single var openAIApiKey : ?Text = null (no getter). Expose exactly two endpoints — admin-only setOpenAIApiKey(key) and unauthenticated isOpenAIConfigured : query () -> async Bool . Same rule: no getOpenAIApiKey / getApiKey endpoint, ever. Fully anonymous: identical to admin-key (single var openAIApiKey : ?Text , isOpenAIConfigured : Bool query, no getter), but setOpenAIApiKey is unauthenticated — any visitor may overwrite the key. Same no-getter / no-log invariants apply. Use only when the spec explicitly says there is no login. 3. is_replicated = ?false is REQUIRED This is the single most important line of code in this skill. Three reasons, in priority order: Security. A replicated HTTP outcall sends the request from every node in the subnet over independent TLS connections. Each connection sees the Authorization: Bearer sk-... header. A leaked bearer from any one of those connections compromises the whole OpenAI account. Billing. Replicated outcalls produce N parallel API calls. OpenAI charges N times. The IC also charges ~13× the cycles of a non-replicated outcall. Determinism. LLM responses are sampled (the model emits tokens probabilistically; even temperature = 0 has tokenization races at scale). Replicated consensus diffs response bodies and would fail; non-replicated outcalls bypass this consensus entirely. → Always: is_replicated = ?false on the Config . 4. Canonical layout This is the default shape. Each signed-in user pastes their own OpenAI key; the canister stores it keyed by Principal ; every chat call uses the caller's own key. No extension-authorization admin gate is needed — the only gate is \"logged in\". The example spans three files: src/backend/main.mo — the actor: state + include s only. src/backend/mixins/openai-chat.mo — the per-user endpoints ( isMyOpenAIConfigured , setMyOpenAIApiKey , clearMyOpenAIApiKey , chat ). src/backend/lib/openai.mo — OpenAI SDK glue (Config builder + chat round-trip). Reused unchanged by §9. import Map \"mo:core/Map\"; import Principal \"mo:core/Principal\"; import AccessControl \"mo:caffeineai-authorization/access-control\"; import MixinAuthorization \"mo:caffeineai-authorization/MixinAuthorization\"; import MixinOpenAIChat \"mixins/openai-chat\"; actor { // Authorization plumbing from extension-authorization. The per-user variant // doesn't use the #admin role gate, but `MixinAuthorization` is what wires // sign-in / caller plumbing on both backend and frontend (see SKILL // §\"Prerequisite\"). let accessControlState : AccessControl.AccessControlState; include MixinAuthorization(accessControlState, null); // Per-user OpenAI keys. Never iterated except by the calling principal. let openAIKeys : Map.Map<Principal, Text>; include MixinOpenAIChat(openAIKeys); }; The migration chain head: import Map \"mo:core/Map\"; import AccessControl \"mo:caffeineai-authorization/access-control\"; module { type NewActor = { accessControlState : AccessControl.AccessControlState; openAIKeys : Map.Map<Principal, Text>; }; public func migration(_old : {}) : NewActor { { accessControlState = AccessControl.initState(); openAIKeys = Map.empty<Principal, Text>(); }; }; }; import Map \"mo:core/Map\"; import Principal \"mo:core/Principal\"; import Runtime \"mo:core/Runtime\"; import OpenAI \"../lib/openai\"; // Per-user OpenAI key endpoints. Mounted by `main.mo` via `include`. // Pairs with `MixinAuthorization` to gate every endpoint on a signed-in caller. mixin (openAIKeys : Map.Map<Principal, Text>) { public query ({ caller }) func isMyOpenAIConfigured() : async Bool { openAIKeys.containsKey(caller); }; public shared ({ caller }) func setMyOpenAIApiKey(key : Text) : async () { if (caller.isAnonymous()) { Runtime.trap(\"Sign in to use this feature\"); }; openAIKeys.add(caller, key); }; public shared ({ caller }) func clearMyOpenAIApiKey() : async () { if (caller.isAnonymous()) { Runtime.trap(\"Sign in to use this feature\"); }; openAIKeys.remove(caller); }; public shared ({ caller }) func chat(prompt : Text) : async Text { if (caller.isAnonymous()) { Runtime.trap(\"Sign in to use this feature\"); }; let ?key = openAIKeys.get(caller) else { Runtime.trap(\"Set your OpenAI API key first\"); }; await* OpenAI.runChatCompletion(OpenAI.configForKey(key), prompt); }; }; import { defaultConfig; type Config } \"mo:openai-client/Config\"; import ChatApi \"mo:openai-client/Apis/ChatApi\"; import CreateChatCompletionRequest \"mo:openai-client/Models/CreateChatCompletionRequest\"; import ChatCompletionRequestUserMessage \"mo:openai-client/Models/ChatCompletionRequestUserMessage\"; import Runtime \"mo:core/Runtime\"; module { // Build a Config bound to a single bearer. `is_replicated = ?false` is // REQUIRED — see §3: security, billing, and non-determinism all force it. public func configForKey(key : Text) : Config { { defaultConfig with auth = ?#bearer key; is_replicated = ?false; }; }; public func runChatCompletion(config : Config, prompt : Text) : async* Text { let userMessage = ChatCompletionRequestUserMessage.JSON.init({ content = #string(prompt); role = #user; }); // `JSON.init` defaults every optional to `null` — DO NOT hand-list them. // Layer optionals with record-update syntax: // { CreateChatCompletionRequest.JSON.init {...} with temperature = ?0.7 } let req = CreateChatCompletionRequest.JSON.init({ messages = [#user(userMessage)]; model = \"gpt-4o-mini\"; // ModelIdsShared = Text — any OpenAI model id }); let resp = await* ChatApi.createChatCompletion(config, req); if (resp.choices.size() == 0) { Runtime.trap(\"OpenAI returned no choices\"); }; resp.choices[0].message.content",
    "model_config": {
        "provider": "deepseek",
        "model": "deepseek-chat",
        "temperature": 0.7,
        "max_tokens": 4096,
        "top_p": 0.9
    },
    "examples": [
        {
            "input": "请用extension-openai帮我处理问题",
            "output": "好的，我是extension-openai。MANDATORY recipe for every Caffeine build that calls OpenAI (ChatGPT, GPT-4o, an LLM, a chatbot, embeddings). The ONLY supported path is the `openai-client` mops package with a canister-side API-key bearer. Hand-rolling `ic.http_request` to `api.openai.com/v1/...` is a FORBIDDEN anti-pattern — it leaks the bearer across replicated outcalls (security + 13× billing impact), bypasses the typed request/response bindings, and forces hand-rolled JSON on a language with poor JSON support. Load this skill whenever the user, spec, or any prior task mentions ChatGPT, GPT (any version), OpenAI, an LLM, a chatbot, or embeddings — and BEFORE writing any code that touches `api.openai.com`. 我会根据你的需求提供专业帮助。"
        },
        {
            "input": "介绍一下你的能力",
            "output": "我是extension-openai，专注于开发编程领域。MANDATORY recipe for every Caffeine build that calls OpenAI (ChatGPT, GPT-4o, an LLM, a chatbot, embeddings). The ONLY supported path is the `openai-client` mops package with a canister-side API-key bearer. Hand-rolling `ic.http_request` to `api.openai.com/v1/...` is a FORBIDDEN anti-pattern — it leaks the bearer across replicated outcalls (security + 13× billing impact), bypasses the typed request/response bindings, and forces hand-rolled JSON on a language with poor JSON support. Load this skill whenever the user, spec, or any prior task mentions ChatGPT, GPT (any version), OpenAI, an LLM, a chatbot, or embeddings — and BEFORE writing any code that touches `api.openai.com`."
        }
    ],
    "install_guide": {
        "coze": "在 Coze 平台创建 Bot -> 技能配置 -> 导入此 .skill 文件",
        "dify": "在 Dify 平台创建应用 -> 添加知识库 -> 导入此 .skill 配置",
        "claude": "将 system_prompt 字段内容复制到 Claude 自定义指令中",
        "custom": "将此 .skill 文件加载到你的 AI Agent 框架中，解析 system_prompt 和 model_config 即可使用"
    },
    "scripts": {
        "python": "# extension-openai - Python extension\n# Add custom Python logic here\ndef process(input_data):\n    return input_data\n",
        "javascript": "// extension-openai - JavaScript extension\n// Add custom JS logic here\nfunction process(inputData) {\n    return inputData;\n}\n"
    },
    "tools": {
        "mcp_servers": [],
        "api_endpoints": []
    },
    "dependencies": {
        "python": [],
        "node": []
    },
    "hooks": {
        "on_load": "echo \"Skill loaded: extension-openai\"",
        "on_call": "",
        "on_error": "echo \"Skill error: please check logs\""
    }
}