@letta-ai/letta-code 0.27.5 → 0.27.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,57 +1,12 @@
1
1
  # Extension provider recipes
2
2
 
3
- Use provider extensions when the user wants Letta Code local agents to use a model provider that is not built into `/connect` and `/model`.
3
+ Use provider extensions when the user wants a **local agent** to use a model provider that is not built into `/connect` and `/model`.
4
4
 
5
- **Important scope:** custom provider extensions are for **local agents only**. They register local provider metadata used by the local backend/TUI/desktop listener. They do not add providers for Constellation/cloud agents.
5
+ Important: provider extensions are local-backend/local-agent only. They register local provider metadata for the TUI, headless local runtime, and desktop listener. They do not add providers for Constellation/cloud agents.
6
6
 
7
7
  For multi-capability extensions that combine a provider with commands, tools, UI, or state, also read `architecture.md`.
8
8
 
9
- ## Contents
10
-
11
- - When to use
12
- - Surface behavior and limitations
13
- - Capability guard
14
- - Basic API-key provider
15
- - Config fields
16
- - Dynamic model discovery
17
- - Connect behavior
18
- - Review checklist
19
-
20
- ## When to use
21
-
22
- Use a provider extension when:
23
-
24
- - the provider API is supported by the local pi-ai adapter stack; OpenAI-compatible providers usually use `api: "openai-completions"` or `api: "openai-responses"`
25
- - the user wants the provider to appear in local `/connect` / desktop Connect model providers
26
- - local `/model` should show static or dynamically listed models from that provider
27
- - local turns should resolve model handles like `kilo/kilo-code`
28
-
29
- Do **not** use a provider extension when the user is asking to configure a Constellation/cloud provider. For cloud agents, use the product's normal provider configuration path instead.
30
-
31
- ## Surface behavior and limitations
32
-
33
- - TUI/headless local agents can load provider extensions along with other extension capabilities.
34
- - The desktop listener loads provider extensions so desktop's local provider UI can list and connect them.
35
- - Listener provider loading is provider-only. Do not rely on commands, tools, UI, events, or `letta.client` while registering a provider.
36
- - Provider extensions should use local data, environment variables, local provider connections, and direct `fetch`/Node APIs. They should not use Constellation-specific APIs to make a local provider work.
37
-
38
- ## Capability guard
39
-
40
- Always guard provider registration:
41
-
42
- ```ts
43
- export default function activate(letta) {
44
- if (!letta.capabilities.providers) return;
45
-
46
- return letta.providers.register("kilo", {
47
- // provider config
48
- });
49
- }
50
- ```
51
-
52
- `letta.registerProvider(id, config)` is also supported, but prefer `letta.providers.register(...)` so the capability being used is explicit.
53
-
54
- ## Basic API-key provider
9
+ ## Quick pattern
55
10
 
56
11
  ```ts
57
12
  // ~/.letta/extensions/kilo.ts
@@ -63,7 +18,8 @@ export default function activate(letta) {
63
18
  description: "Connect to Kilo's OpenAI-compatible API",
64
19
  api: "openai-completions",
65
20
  baseUrl: "https://api.kilo.example/v1",
66
- apiKey: "KILO_API_KEY",
21
+ apiKey: "KILO_API_KEY", // env var name, not the raw secret
22
+ authHeader: true,
67
23
  models: [
68
24
  {
69
25
  id: "kilo-code",
@@ -73,136 +29,82 @@ export default function activate(letta) {
73
29
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
74
30
  contextWindow: 128000,
75
31
  maxTokens: 8192,
32
+ compat: {
33
+ supportsDeveloperRole: false,
34
+ supportsReasoningEffort: false,
35
+ },
76
36
  },
77
37
  ],
78
38
  connect: {
79
- fields: [
80
- { key: "apiKey", label: "Kilo API Key", secret: true },
81
- ],
39
+ fields: [{ key: "apiKey", label: "Kilo API Key", secret: true }],
82
40
  },
83
41
  });
84
42
  }
85
43
  ```
86
44
 
87
- After `/reload`, this provider appears in local `/connect` and in the desktop local provider page. The model handle is `<provider-id>/<model-id>`, for example `kilo/kilo-code`.
45
+ After `/reload`, the provider appears in local `/connect` and desktop Connect model providers. Model handles are `<provider-id>/<model-id>`, for example `kilo/kilo-code`.
88
46
 
89
- ## Config fields
47
+ ## Key rules
90
48
 
91
- Common provider config fields:
49
+ - Always guard with `letta.capabilities.providers`.
50
+ - Prefer `letta.providers.register(...)` over legacy `letta.registerProvider(...)`.
51
+ - Keep provider registration independent from commands/tools/UI/events and `letta.client`; the desktop listener loads provider-only extensions.
52
+ - Do not hardcode real secrets. `apiKey: "ENV_VAR"` resolves `process.env.ENV_VAR` when present, or lets `/connect` save a local key.
53
+ - Use stable lowercase provider ids. Model ids must be unprefixed and must not contain `/`.
54
+ - Set `api` at provider or model level. Common values include `"openai-completions"`, `"openai-responses"`, `"anthropic-messages"`, and `"bedrock-converse-stream"`; check `src/backend/dev/pi-provider-extension-types.ts` and pi-ai model types before using uncommon values.
92
55
 
93
- ```ts
94
- {
95
- name?: string;
96
- description?: string;
97
- api?: string; // common: "openai-completions", "openai-responses", "anthropic-messages", "bedrock-converse-stream"
98
- baseUrl?: string;
99
- apiKey?: string; // env var name to resolve, e.g. "KILO_API_KEY"
100
- headers?: Record<string, string>; // values resolve through process.env when present
101
- authHeader?: boolean; // add Authorization: Bearer <apiKey>
102
- models?: Array<model>;
103
- listModels?: (connection) => Promise<Array<model>> | Array<model>;
104
- connect?: boolean | { fields?: Array<field> };
105
- }
106
- ```
56
+ ## Model metadata
107
57
 
108
- Check `src/backend/dev/pi-provider-extension-types.ts` and pi-ai model types before documenting or using an uncommon `api` value.
109
-
110
- Model entries must include useful local runtime metadata:
58
+ Each model needs enough local runtime metadata for selection and context display:
111
59
 
112
60
  ```ts
113
61
  {
114
62
  id: "model-id-without-provider-prefix",
115
63
  name: "Display Name",
116
- api?: "openai-completions",
117
64
  reasoning: false,
118
- input: ["text"], // or ["text", "image"]
65
+ input: ["text"], // or ["text", "image"]
119
66
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
120
67
  contextWindow: 128000,
121
68
  maxTokens: 8192,
122
- headers?: {},
69
+ // Optional: compat: { supportsDeveloperRole: false, supportsReasoningEffort: false }
123
70
  }
124
71
  ```
125
72
 
126
- Do not set `model.baseUrl` when all models use the provider-level URL. Model-level `baseUrl` overrides the connected provider base URL, so `/connect` base URL overrides will be ignored. Use it only when a specific model intentionally needs a different endpoint.
73
+ Do not set `model.baseUrl` when all models use the provider-level URL. Model-level `baseUrl` overrides the connected provider base URL, so `/connect` base URL overrides are ignored. Use it only when a specific model intentionally needs a different endpoint.
74
+
75
+ ## Connect fields
76
+
77
+ - `connect: undefined` / `true` uses default API key + base URL fields.
78
+ - `connect: { fields: [...] }` customizes local `/connect` / desktop fields.
79
+ - `connect: false` hides the provider from `/connect`; use only when credentials come entirely from env/local code.
80
+ - Custom fields are currently required. TUI pre-fills non-secret placeholders for convenience, but placeholders are not backend/protocol defaults.
81
+ - If the provider has a normal fixed `baseUrl`, set provider-level `baseUrl` and omit `baseUrl` from `connect.fields`; the local runtime uses the provider-level URL after the API key is saved.
82
+ - Include `{ key: "baseUrl", ... }` only when the user must enter or review/override the endpoint during connect.
127
83
 
128
84
  ## Dynamic model discovery
129
85
 
130
- Use `listModels(connection)` when the provider exposes a models endpoint or the model list depends on connected credentials:
86
+ Use `listModels(connection)` only when the provider exposes a models endpoint or the model list depends on credentials:
131
87
 
132
88
  ```ts
133
- export default function activate(letta) {
134
- if (!letta.capabilities.providers) return;
135
-
136
- return letta.providers.register("kilo", {
137
- name: "Kilo",
138
- api: "openai-completions",
139
- baseUrl: "https://api.kilo.example/v1",
140
- apiKey: "KILO_API_KEY",
141
- async listModels(connection) {
142
- const response = await fetch(`${connection.baseUrl}/models`, {
143
- headers: {
144
- Authorization: `Bearer ${connection.apiKey}`,
145
- ...connection.headers,
146
- },
147
- });
148
- if (!response.ok) {
149
- throw new Error(`Kilo model list failed: ${response.status}`);
150
- }
151
- const body = await response.json();
152
- return body.data.map((model) => ({
153
- id: model.id,
154
- name: model.id,
155
- reasoning: false,
156
- input: ["text"],
157
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
158
- contextWindow: 128000,
159
- maxTokens: 8192,
160
- }));
89
+ async listModels(connection) {
90
+ const response = await fetch(`${connection.baseUrl}/models`, {
91
+ headers: {
92
+ Authorization: `Bearer ${connection.apiKey}`,
93
+ ...connection.headers,
161
94
  },
162
95
  });
96
+ if (!response.ok) throw new Error(`Model list failed: ${response.status}`);
97
+ const body = await response.json();
98
+ return body.data.map((model) => ({
99
+ id: model.id,
100
+ name: model.id,
101
+ reasoning: false,
102
+ input: ["text"],
103
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
104
+ contextWindow: 128000,
105
+ maxTokens: 8192,
106
+ }));
163
107
  }
164
108
  ```
165
109
 
166
- `connection` contains the provider id/name plus resolved local connection details:
167
-
168
- ```ts
169
- {
170
- id: string;
171
- providerName: string;
172
- baseUrl?: string;
173
- apiKey?: string;
174
- headers?: Record<string, string>;
175
- }
176
- ```
177
-
178
- Keep dynamic listing lightweight and fail with short actionable errors. If listing is flaky, provide a static `models` fallback instead.
179
-
180
- ## Connect behavior
181
-
182
- - `connect: undefined` or `connect: true` uses default API key + base URL fields.
183
- - `connect: { fields: [...] }` customizes local `/connect` / desktop fields.
184
- - `connect: false` hides the provider from `/connect`; use only when credentials come entirely from environment variables or local code.
185
- - `apiKey: "ENV_VAR_NAME"` resolves from `process.env.ENV_VAR_NAME` when available. Do not hardcode real secrets.
186
- - Custom fields are currently required. `placeholder` is display-only, not a default value.
187
- - If the provider has a normal fixed `baseUrl`, set provider-level `baseUrl` and omit `baseUrl` from `connect.fields`; the local runtime will use the provider-level URL after the API key is saved.
188
- - Include `{ key: "baseUrl", ... }` only when the user must enter or override the endpoint during connect.
189
-
190
- Default custom fields use these keys when possible because the local provider store understands them:
191
-
192
- ```ts
193
- { key: "apiKey", label: "API Key", secret: true }
194
- { key: "baseUrl", label: "Base URL" }
195
- ```
196
-
197
- ## Review checklist
198
-
199
- - The extension says or implies the provider is local-agent only when reporting back to the user.
200
- - `letta.capabilities.providers` is checked before registration.
201
- - Provider id is stable, lowercase, and suitable as the model-handle prefix.
202
- - Model ids are unprefixed; Letta Code exposes them as `<provider-id>/<model-id>`.
203
- - `contextWindow`, `maxTokens`, `input`, and `reasoning` metadata are accurate enough for local selection and context display.
204
- - Model-level `baseUrl` is omitted unless the model intentionally needs a different endpoint from the provider/connection.
205
- - `connect.fields` contains only fields the user must type; placeholders are not treated as defaults.
206
- - Secrets are read from environment/local provider connections, not hardcoded.
207
- - Provider registration does not depend on commands/tools/UI/events or `letta.client`.
208
- - The user is told to run `/reload`, then configure the provider through local `/connect` or desktop Connect model providers.
110
+ `connection` has `{ id, providerName, baseUrl?, apiKey?, headers? }`. Keep dynamic listing lightweight; if it is flaky, prefer static `models`.
@@ -5,7 +5,7 @@ description: Creates, edits, and enables Letta Code extension-provided slash com
5
5
 
6
6
  # Customizing Commands
7
7
 
8
- Use this as the command-specific entrypoint for local extension slash commands. For broader extension work, recipes live in `../creating-extensions/references/commands.md`, `../creating-extensions/references/architecture.md`, `../creating-extensions/references/ui.md`, and `../creating-extensions/references/btw-command.md`.
8
+ Use this as the command-specific entrypoint for local extension slash commands. For broader extension work, recipes live in `../creating-extensions/references/commands.md`, `../creating-extensions/references/architecture.md`, `../creating-extensions/references/ui.md`, and `../creating-extensions/references/plan-mode.md`.
9
9
 
10
10
  Extension files live in:
11
11
 
@@ -87,4 +87,4 @@ type ExtensionCommandResult =
87
87
  - Simple output command, panel command, busy-safe conversation command: `../creating-extensions/references/commands.md`
88
88
  - Complex command architecture, state, cleanup: `../creating-extensions/references/architecture.md`
89
89
  - Panel/status UI patterns: `../creating-extensions/references/ui.md`
90
- - Complete `/btw` side-question recipe: `../creating-extensions/references/btw-command.md`
90
+ - Worked plan-mode command/tool composition: `../creating-extensions/references/plan-mode.md`
@@ -1,106 +0,0 @@
1
- # `/btw` side-question extension example
2
-
3
- This example runs while the main agent is busy because it forks the scoped conversation, streams a response in the fork, renders progress in a panel when panels are available, and returns `{ type: "handled" }` immediately.
4
-
5
- ```ts
6
- export default function activate(letta) {
7
- if (!letta.capabilities.commands) return;
8
-
9
- function createOtid() {
10
- return (
11
- globalThis.crypto?.randomUUID?.() ??
12
- `btw-${Date.now()}-${Math.random().toString(16).slice(2)}`
13
- );
14
- }
15
-
16
- function appendAssistantText(chunk, parts) {
17
- if (chunk.message_type !== "assistant_message") return;
18
- const content = chunk.content;
19
- if (typeof content === "string") {
20
- parts.push(content);
21
- return;
22
- }
23
- if (Array.isArray(content)) {
24
- for (const part of content) {
25
- if (part && typeof part === "object" && "text" in part) {
26
- parts.push(String(part.text));
27
- }
28
- }
29
- return;
30
- }
31
- if (content && typeof content === "object" && "text" in content) {
32
- parts.push(String(content.text));
33
- }
34
- }
35
-
36
- function openPanelOrNull(content) {
37
- if (!letta.capabilities.ui.panels) return null;
38
- return letta.ui.openPanel({ id: "btw", content });
39
- }
40
-
41
- return letta.commands.register({
42
- id: "btw",
43
- description: "Ask a side question in a forked conversation",
44
- args: "<question>",
45
- runWhenBusy: true,
46
- showInTranscript: false,
47
- run(ctx) {
48
- const question = ctx.args.trim();
49
- if (!question) {
50
- const panel = openPanelOrNull(["/btw", "Usage: /btw <question>"]);
51
- if (panel) setTimeout(() => panel.close(), 5_000);
52
- return { type: "handled" };
53
- }
54
-
55
- const panel = openPanelOrNull([`/btw ${question}`, "..."]);
56
-
57
- void (async () => {
58
- try {
59
- const forked = await ctx.conversation.fork({ hidden: true });
60
- const stream = await forked.sendMessageStream(
61
- [
62
- {
63
- role: "user",
64
- content: `${question}\n\nAnswer briefly in 1-3 short sentences.`,
65
- otid: createOtid(),
66
- },
67
- ],
68
- {
69
- overrideModel: ctx.model.id ?? undefined,
70
- workingDirectory: ctx.cwd,
71
- },
72
- );
73
-
74
- const parts = [];
75
- for await (const chunk of stream) {
76
- appendAssistantText(chunk, parts);
77
- panel?.update({
78
- content: [`/btw ${question}`, parts.join("") || "..."],
79
- });
80
- }
81
-
82
- panel?.update({
83
- content: [
84
- `done /btw ${question}`,
85
- parts.join("").trim() || "No response.",
86
- ],
87
- });
88
- if (panel) setTimeout(() => panel.close(), 10_000);
89
- } catch (error) {
90
- panel?.update({
91
- content: [
92
- `error /btw ${question}`,
93
- error instanceof Error ? error.message : String(error),
94
- ],
95
- });
96
- if (panel) setTimeout(() => panel.close(), 15_000);
97
- }
98
- })();
99
-
100
- return { type: "handled" };
101
- },
102
- });
103
- }
104
- ```
105
-
106
- Add custom borders, right alignment, wrapping, or history only if the user asks for that polish.