E

extension-openai

by @caffeinelabsv
4.6(120)

この Skill は、開発者が OpenAI/LLM 機能をアプリケーションに統合するための効率的なソリューションです。`openai-client` コネクタを介して OpenAI のチャットおよび埋め込み API へ簡単にアクセスでき、API キー管理を簡素化し、安全な外部呼び出しを保証します。これにより、開発者はスマートなチャットボットやセマンティック検索など、高度な AI 駆動型アプリケーションを迅速に構築できます。

openaillmapi-integrationbackendai-developmentGitHub
インストール方法
npx skills add https://github.com/caffeinelabs/skills --skill extension-openai
compare_arrows

Before / After 効果比較

1 组
使用前

開発者は HTTP リクエストの手動作成、API キーの保存、安全な呼び出しロジックの実装に多くの時間を費やし、エラーも発生しやすく、統合期間が長期化していました。

使用後

この Skill は標準化された `openai-client` コネクタとキー管理を提供し、統合プロセスを大幅に簡素化し、アプリケーション開発を加速します。

SKILL.md

OpenAI integration

OpenAI / LLM extension for Caffeine AI.

Orchestrator routing notes

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; 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 intentPlatform capability
Use ChatGPT / GPT-4o / "an LLM" to answer / summarise / classify textopenai-client connector (ChatApi via extension-openai skill)
Build a chatbot / AI assistantopenai-client connector (ChatApi via extension-openai skill)
Generate embeddings for similarity searchopenai-client connector (EmbeddingsApi via extension-openai skill)

Backend

Use this skill whenever the user wants their canister to call OpenAI. The ingredients are:

  1. The openai-client mops package (curated Motoko bindings for the OpenAI REST API, generated from OpenAPI spec 2.3.0).
  2. 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.
  3. 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

VariantWho pastes the keyWho paysSetter gateUse 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:

  1. 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.
  2. Billing. Replicated outcalls produce N parallel API calls. OpenAI charges N times. The IC also charges ~13× the cycles of a non-replicated outcall.
  3. 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 + includes 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.initState();
  include MixinAuthorization(accessControlState);

  // Per-user OpenAI keys. Never iterated except by the calling principal.
  let openAIKeys : Map.Map<Principal, Text> = Map.empty();
  include MixinOpenAIChat(openAIKeys);
};
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");
    };
    switch (resp.choices[0].message.content) {
      case (?text) text;
      case null Runtime.trap("OpenAI returned no text content (refusal or tool call)");
    };
  };
};

Per-user-specific invariants

  • Key the map by caller, never by user-supplied id. A Text userId from the frontend can be spoofed; Principal from shared ({ caller }) cannot.
  • No endpoint ever returns the key — not another user's, not even the caller's own. The frontend learns "configured? yes/no" from isMyOpenAIConfigured : async Bool and nothing more. Concretely: do not generate getMyOpenAIApiKey, getApiKey, myApiKey, or any other shared / query function whose return type is ?Text / Text. Internal reads of the map (inside chat, configFor, etc.) use openAIKeys.get(caller) and never escape the canister boundary. An iterator or a key-returning endpoint leaks every user's bearer.
  • Trap cleanly when the key is missing. Use Runtime.trap("Set your OpenAI API key first") (or return a typed error) — the message identifies whose key is missing without leaking it.
  • Anonymous callers must not store keys. caller.isAnonymous() short-circuits before any openAIKeys.add — otherwise everyone reading the canister via 2vxsx-fae shares one key slot.
  • stable var / migration. The Map<Principal, Text> lives in stable memory like any other actor field; on upgrade, decide whether to preserve, rotate, or drop the keys. The default (preserve) is correct for almost all apps. If you ever rotate, drop the whole map — never partially.

...

ユーザーレビュー (0)

レビューを書く

効果
使いやすさ
ドキュメント
互換性

レビューなし

統計データ

インストール数27.5K
評価4.6 / 5.0
バージョン
更新日2026年10月23日
比較事例1 件

ユーザー評価

4.6(120)
5
37%
4
43%
3
13%
2
5%
1
2%

この Skill を評価

0.0

対応プラットフォーム

🤖claude-code

タイムライン

作成2026年6月25日
最終更新2026年10月23日
🎁 Agent 知識カード
アンケート