pi-commandcode-provider 0.6.2 → 0.6.3
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.
- package/CHANGELOG.md +9 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +2 -0
- package/index.ts +42 -12
- package/package.json +1 -1
- package/src/converters.ts +21 -4
- package/src/core.ts +5 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.6.3 - 2026-09-02
|
|
6
|
+
|
|
7
|
+
- Fix Oh My Pi chat returning `401 Invalid 'Authorization' header` after `/login`: OMP kept the unresolved `$COMMAND_CODE_API_KEY` placeholder as a literal config API key that shadowed its stored credentials and was sent as the Bearer token. The placeholder is now registered only on pi, where it keeps the API-key login method and `--api-key` working next to OAuth; on OMP the provider omits `apiKey` unless a real key is configured. Placeholders passed by the host are also resolved or stripped on the Provider API and compat stream paths, and the legacy generate transport resolves its key through the same rule.
|
|
8
|
+
- Cover stored `/login` OAuth and API-key credentials, `--api-key`, and env keys end to end on both pi and Oh My Pi, asserting the exact Bearer token the mock API receives. CI now runs the pi end-to-end suite against a real `pi` binary instead of skipping it.
|
|
9
|
+
|
|
10
|
+
### Contributors
|
|
11
|
+
|
|
12
|
+
- @ebreen — reported and diagnosed the Oh My Pi `/login` 401, and opened the fix that this release builds on (#78).
|
|
13
|
+
|
|
5
14
|
## 0.6.2 - 2026-09-02
|
|
6
15
|
|
|
7
16
|
- Fix `omp plugin install` on Oh My Pi 18.x, which rejected 0.6.1 because its pi-ai lacks the `registerApiProvider` export; the compat registration now resolves at runtime and is skipped on hosts that register custom APIs themselves.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -52,6 +52,10 @@ COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key npm run test:e2e:liv
|
|
|
52
52
|
|
|
53
53
|
Use `npm run test:e2e:live:all` with the Go and GOAT file variables to run both subscription transports sequentially. Store keys in a secret manager and export each one to a new mode-`0600` temporary file for the test; never add key files to the repository. Direct `*_API_KEY` variables are intended primarily for protected CI secrets.
|
|
54
54
|
|
|
55
|
+
### pi end-to-end
|
|
56
|
+
|
|
57
|
+
`tests/test-pi-local.mjs` runs the extension inside a real `pi` binary against a mock Command Code API, including every credential source (`/login` OAuth and API-key credentials, `--api-key`, env keys). It skips locally when `pi` is not on `PATH`; CI installs pi and runs it as part of `npm test` with `PI_LOCAL_REQUIRED=1`. Point `PI_BIN` at another pi executable to test against a specific version.
|
|
58
|
+
|
|
55
59
|
### Oh My Pi compatibility
|
|
56
60
|
|
|
57
61
|
`tests/test-omp-compat.mjs` runs the extension inside a real `omp` binary against a mock Command Code API. It skips locally when `omp` is not on `PATH`; CI installs Oh My Pi and runs it as a required check with `OMP_COMPAT_REQUIRED=1`, so a change that only loads on pi fails CI instead of the next `omp plugin install`.
|
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ Run `/login` in pi or OMP. Select **Use a subscription**, then **Command Code**.
|
|
|
41
41
|
|
|
42
42
|
If automatic transfer from the browser fails, copy the API key shown by Command Code and paste it into the terminal prompt.
|
|
43
43
|
|
|
44
|
+
On Oh My Pi, `/login` stores those credentials in OMP's credential store and chat uses them directly. If chat still returns `401 Invalid 'Authorization' header`, restart OMP after `/login` and confirm `/commandcode-quota` shows your account.
|
|
45
|
+
|
|
44
46
|
### Environment variable
|
|
45
47
|
|
|
46
48
|
```sh
|
package/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { join } from "node:path"
|
|
18
18
|
|
|
19
19
|
import { getConfiguredApiKey } from "./src/api-key.ts"
|
|
20
|
+
import { pickCommandCodeApiKey, withResolvedCommandCodeApiKey } from "./src/converters.ts"
|
|
20
21
|
import { createStreamCommandCode } from "./src/core.ts"
|
|
21
22
|
import { calculateCommandCodeCost } from "./src/cost.ts"
|
|
22
23
|
import {
|
|
@@ -54,10 +55,40 @@ type CompatStreamFunction = (
|
|
|
54
55
|
* and registers custom APIs itself inside `registerProvider`. Resolve the
|
|
55
56
|
* function at runtime so the extension loads on both hosts.
|
|
56
57
|
*/
|
|
57
|
-
function
|
|
58
|
+
function compatApiProviderRegistrar(): ((...args: unknown[]) => unknown) | undefined {
|
|
58
59
|
const register = (piAiCompat as { registerApiProvider?: unknown }).registerApiProvider
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
return typeof register === "function" ? (register as (...args: unknown[]) => unknown) : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function registerCompatApiProvider(stream: CompatStreamFunction): void {
|
|
64
|
+
compatApiProviderRegistrar()?.(
|
|
65
|
+
{ api: COMMAND_CODE_API, stream, streamSimple: stream },
|
|
66
|
+
COMPAT_SOURCE_ID,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The `apiKey` handed to `registerProvider` means different things per host.
|
|
72
|
+
*
|
|
73
|
+
* pi parses `$COMMAND_CODE_API_KEY` as an env template: unresolved means
|
|
74
|
+
* "not configured", so `/login` credentials and `--api-key` take over, and
|
|
75
|
+
* the entry keeps the API-key auth method registered next to OAuth. Without
|
|
76
|
+
* it pi composes an OAuth-only provider and drops stored `api_key`
|
|
77
|
+
* credentials and `--api-key`.
|
|
78
|
+
*
|
|
79
|
+
* Oh My Pi has no template notion: an unresolved value stays a literal config
|
|
80
|
+
* override that shadows its `/login` credential store and is sent verbatim as
|
|
81
|
+
* `Authorization: Bearer $COMMAND_CODE_API_KEY`. There, omit `apiKey` unless
|
|
82
|
+
* a real key is configured; OMP then reads env keys and stored credentials
|
|
83
|
+
* itself.
|
|
84
|
+
*
|
|
85
|
+
* Hosts are told apart by the same `registerApiProvider` probe used for the
|
|
86
|
+
* compat registry: pi exports it, OMP does not.
|
|
87
|
+
*/
|
|
88
|
+
function providerApiKey(): string | undefined {
|
|
89
|
+
const configured = pickCommandCodeApiKey(getConfiguredApiKey(), undefined)
|
|
90
|
+
if (configured) return configured
|
|
91
|
+
return compatApiProviderRegistrar() ? "$COMMAND_CODE_API_KEY" : undefined
|
|
61
92
|
}
|
|
62
93
|
|
|
63
94
|
function commandCodeHeaders(): Record<string, string> | undefined {
|
|
@@ -76,7 +107,7 @@ function createProviderConfig(
|
|
|
76
107
|
return {
|
|
77
108
|
name: "Command Code",
|
|
78
109
|
baseUrl: apiBase,
|
|
79
|
-
apiKey:
|
|
110
|
+
apiKey: providerApiKey(),
|
|
80
111
|
api: COMMAND_CODE_API,
|
|
81
112
|
streamSimple: streamCommandCode,
|
|
82
113
|
headers,
|
|
@@ -132,15 +163,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
132
163
|
calculateCost: calculateCommandCodeCost,
|
|
133
164
|
apiBase: legacyApiBase(apiBase),
|
|
134
165
|
})
|
|
166
|
+
const resolveStreamOptions = (options?: Parameters<typeof streamNativeProvider>[2]) =>
|
|
167
|
+
withResolvedCommandCodeApiKey(options, getConfiguredApiKey())
|
|
135
168
|
const transport = createCommandCodeTransportRouter({
|
|
136
169
|
createStream: () => new AssistantMessageEventStream(),
|
|
137
170
|
streamProvider: (model, context, options) =>
|
|
138
171
|
streamNativeProvider(
|
|
139
172
|
{ ...model, api: apiForModelId(model.id), compat: model.compatConfig ?? model.compat },
|
|
140
173
|
context,
|
|
141
|
-
options,
|
|
174
|
+
resolveStreamOptions(options),
|
|
142
175
|
),
|
|
143
|
-
streamGenerate,
|
|
176
|
+
streamGenerate: (model, context, options) =>
|
|
177
|
+
streamGenerate(model, context, resolveStreamOptions(options)),
|
|
144
178
|
})
|
|
145
179
|
|
|
146
180
|
// pi dispatches the main chat through the registered provider, but sibling
|
|
@@ -149,13 +183,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
149
183
|
// api-registry, which knows nothing about extension providers. Register the
|
|
150
184
|
// custom api there so those calls reach the same transport. The registry
|
|
151
185
|
// resolves no credentials for extension providers, so fall back to the
|
|
152
|
-
// configured key when the caller passes none.
|
|
186
|
+
// configured key when the caller passes none or a placeholder.
|
|
153
187
|
const compatStream: CompatStreamFunction = (model, context, options) =>
|
|
154
|
-
transport.stream(
|
|
155
|
-
model,
|
|
156
|
-
context,
|
|
157
|
-
options?.apiKey ? options : { ...options, apiKey: getConfiguredApiKey() },
|
|
158
|
-
) as AssistantMessageEventStream
|
|
188
|
+
transport.stream(model, context, resolveStreamOptions(options)) as AssistantMessageEventStream
|
|
159
189
|
registerCompatApiProvider(compatStream)
|
|
160
190
|
|
|
161
191
|
pi.on("message_end", async (event, ctx) => {
|
package/package.json
CHANGED
package/src/converters.ts
CHANGED
|
@@ -147,6 +147,13 @@ export const COMMAND_CODE_PLACEHOLDER_KEYS = new Set([
|
|
|
147
147
|
"COMMANDCODE_API_KEY",
|
|
148
148
|
])
|
|
149
149
|
|
|
150
|
+
function usableCommandCodeApiKey(value: string | undefined): string | undefined {
|
|
151
|
+
const trimmed = typeof value === "string" ? value.trim() : undefined
|
|
152
|
+
if (!trimmed) return undefined
|
|
153
|
+
if (COMMAND_CODE_PLACEHOLDER_KEYS.has(trimmed)) return undefined
|
|
154
|
+
return trimmed
|
|
155
|
+
}
|
|
156
|
+
|
|
150
157
|
/**
|
|
151
158
|
* Pick the real API key from a host registry value and/or the env/auth-file
|
|
152
159
|
* fallback, never returning a literal placeholder or an empty/whitespace value.
|
|
@@ -156,10 +163,20 @@ export function pickCommandCodeApiKey(
|
|
|
156
163
|
registryKey: string | undefined,
|
|
157
164
|
hostKey: string | undefined,
|
|
158
165
|
): string | undefined {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
return usableCommandCodeApiKey(registryKey) ?? usableCommandCodeApiKey(hostKey)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Replace a host-supplied placeholder (or missing key) with the configured
|
|
171
|
+
* fallback. Used for both registerProvider and the Provider API stream path.
|
|
172
|
+
*/
|
|
173
|
+
export function withResolvedCommandCodeApiKey<T extends { apiKey?: string }>(
|
|
174
|
+
options: T | undefined,
|
|
175
|
+
configuredKey: string | undefined,
|
|
176
|
+
): T | { apiKey?: string } {
|
|
177
|
+
const apiKey = pickCommandCodeApiKey(options?.apiKey, configuredKey)
|
|
178
|
+
if (options && apiKey === options.apiKey) return options
|
|
179
|
+
return { ...options, apiKey }
|
|
163
180
|
}
|
|
164
181
|
|
|
165
182
|
export function textContent(message: { content?: unknown }): string {
|
package/src/core.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
messagesToCC,
|
|
20
20
|
numberValue,
|
|
21
21
|
parseStreamEventLine,
|
|
22
|
+
pickCommandCodeApiKey,
|
|
22
23
|
recordOrEmpty,
|
|
23
24
|
stringValue,
|
|
24
25
|
toolsToJson,
|
|
@@ -237,22 +238,14 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
237
238
|
|
|
238
239
|
async function run() {
|
|
239
240
|
// Some hosts pass a literal env-var reference instead of resolving it.
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
"COMMAND_CODE_API_KEY",
|
|
243
|
-
"$COMMANDCODE_API_KEY",
|
|
244
|
-
"COMMANDCODE_API_KEY",
|
|
245
|
-
])
|
|
246
|
-
const hostKey =
|
|
247
|
-
options?.apiKey && !PLACEHOLDER_API_KEYS.has(options.apiKey) ? options.apiKey : undefined
|
|
248
|
-
|
|
249
|
-
const apiKey =
|
|
250
|
-
hostKey ??
|
|
241
|
+
const apiKey = pickCommandCodeApiKey(
|
|
242
|
+
options?.apiKey,
|
|
251
243
|
getApiKey({
|
|
252
244
|
env: deps.env,
|
|
253
245
|
authPaths: deps.authPaths,
|
|
254
246
|
homeDir: deps.homeDir,
|
|
255
|
-
})
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
256
249
|
|
|
257
250
|
if (!apiKey) {
|
|
258
251
|
const msg: AssistantMessageLike = {
|