@gotgenes/pi-permission-system 0.8.0 → 1.1.0
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 +32 -0
- package/README.md +0 -1
- package/package.json +1 -1
- package/schemas/permissions.schema.json +0 -11
- package/src/index.ts +7 -3
- package/src/permission-manager.ts +50 -5
- package/tests/permission-system.test.ts +89 -2
- package/src/model-option-compatibility.ts +0 -182
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0](https://github.com/gotgenes/pi-permission-system/compare/v1.0.0...v1.1.0) (2026-05-03)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* emit deprecation warning for special.tool_call_limit ([#18](https://github.com/gotgenes/pi-permission-system/issues/18)) ([1170d40](https://github.com/gotgenes/pi-permission-system/commit/1170d401d3adc438ad3c69bf96f5264b981ed4d5))
|
|
14
|
+
* notify user of deprecated config fields at startup ([#18](https://github.com/gotgenes/pi-permission-system/issues/18)) ([3408672](https://github.com/gotgenes/pi-permission-system/commit/3408672afb94783f173b579e9e33fe088f0971f3))
|
|
15
|
+
* surface config issues from PermissionManager ([#18](https://github.com/gotgenes/pi-permission-system/issues/18)) ([4c8103b](https://github.com/gotgenes/pi-permission-system/commit/4c8103bed99c3d31813f5450a6f4d1938fd74f25))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
* plan drop unread special.tool_call_limit from schema ([#18](https://github.com/gotgenes/pi-permission-system/issues/18)) ([c45f6f7](https://github.com/gotgenes/pi-permission-system/commit/c45f6f7e5a504cac4f6156a97f51ff9588639d94))
|
|
21
|
+
* remove tool_call_limit from schema and README ([#18](https://github.com/gotgenes/pi-permission-system/issues/18)) ([780b414](https://github.com/gotgenes/pi-permission-system/commit/780b41431b1ac06ccf11dca00e1c59575386bbd2))
|
|
22
|
+
|
|
23
|
+
## [1.0.0](https://github.com/gotgenes/pi-permission-system/compare/v0.8.0...v1.0.0) (2026-05-03)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### ⚠ BREAKING CHANGES
|
|
27
|
+
|
|
28
|
+
* The bundled temperature-stripping shim for OpenAI Responses-style APIs (openai-codex-responses, openai-responses, azure-openai-responses) has been removed. This module monkey-patched the provider stack at the process level and had no connection to permission enforcement. Users who need the shim can extract it into a standalone extension.
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
* remove out-of-scope model-option-compatibility provider shim ([#17](https://github.com/gotgenes/pi-permission-system/issues/17)) ([b390896](https://github.com/gotgenes/pi-permission-system/commit/b39089611fe565f81dacf7fa0bff3af36d50f7ce))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Documentation
|
|
36
|
+
|
|
37
|
+
* plan removal of out-of-scope model-option-compatibility shim ([#17](https://github.com/gotgenes/pi-permission-system/issues/17)) ([a48f2ef](https://github.com/gotgenes/pi-permission-system/commit/a48f2ef025cbe970d21071b94552fcb9a4f7de89))
|
|
38
|
+
* **retro:** add retro notes for issue [#16](https://github.com/gotgenes/pi-permission-system/issues/16) ([ee710cb](https://github.com/gotgenes/pi-permission-system/commit/ee710cba9b1fe92a39016c61348d2a0eb2895d1f))
|
|
39
|
+
|
|
8
40
|
## [0.8.0](https://github.com/gotgenes/pi-permission-system/compare/v0.7.0...v0.8.0) (2026-05-03)
|
|
9
41
|
|
|
10
42
|
|
package/README.md
CHANGED
|
@@ -328,7 +328,6 @@ Reserved permission checks:
|
|
|
328
328
|
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
329
329
|
| `doom_loop` | Controls doom loop detection behavior |
|
|
330
330
|
| `external_directory` | Enforces ask/allow/deny decisions for path-bearing built-in tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) when they target paths outside the active working directory |
|
|
331
|
-
| `tool_call_limit` | _(schema only, not enforced yet)_ |
|
|
332
331
|
|
|
333
332
|
```jsonc
|
|
334
333
|
{
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
savePermissionSystemConfig,
|
|
36
36
|
} from "./extension-config.js";
|
|
37
37
|
import { createPermissionSystemLogger, safeJsonStringify } from "./logging.js";
|
|
38
|
-
import { registerModelOptionCompatibilityGuard } from "./model-option-compatibility.js";
|
|
39
38
|
import {
|
|
40
39
|
isPermissionDecisionState,
|
|
41
40
|
type PermissionPromptDecision,
|
|
@@ -1338,8 +1337,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1338
1337
|
|
|
1339
1338
|
setLoggingWarningReporter(notifyWarning);
|
|
1340
1339
|
refreshExtensionConfig();
|
|
1341
|
-
registerModelOptionCompatibilityGuard(pi);
|
|
1342
|
-
|
|
1343
1340
|
registerPermissionSystemCommand(pi, {
|
|
1344
1341
|
getConfig: () => extensionConfig,
|
|
1345
1342
|
setConfig: saveExtensionConfig,
|
|
@@ -1572,6 +1569,13 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1572
1569
|
startForwardedPermissionPolling(ctx);
|
|
1573
1570
|
logResolvedConfigPaths();
|
|
1574
1571
|
|
|
1572
|
+
const policyIssues = permissionManager.getConfigIssues(
|
|
1573
|
+
lastKnownActiveAgentName,
|
|
1574
|
+
);
|
|
1575
|
+
for (const issue of policyIssues) {
|
|
1576
|
+
notifyWarning(issue);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1575
1579
|
if (event.reason === "reload") {
|
|
1576
1580
|
writeDebugLog("lifecycle.reload", {
|
|
1577
1581
|
triggeredBy: "session_start",
|
|
@@ -237,8 +237,18 @@ function getConfiguredMcpServerNamesFromPaths(
|
|
|
237
237
|
);
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
const DEPRECATED_SPECIAL_KEYS: ReadonlySet<string> = new Set([
|
|
241
|
+
"tool_call_limit",
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
export interface NormalizeResult {
|
|
245
|
+
permissions: AgentPermissions;
|
|
246
|
+
configIssues: string[];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function normalizeRawPermission(raw: unknown): NormalizeResult {
|
|
241
250
|
const record = toRecord(raw);
|
|
251
|
+
const configIssues: string[] = [];
|
|
242
252
|
const normalizedTools = normalizePermissionRecord(record.tools);
|
|
243
253
|
|
|
244
254
|
const normalized: AgentPermissions = {
|
|
@@ -250,6 +260,20 @@ function normalizeRawPermission(raw: unknown): AgentPermissions {
|
|
|
250
260
|
special: normalizePermissionRecord(record.special),
|
|
251
261
|
};
|
|
252
262
|
|
|
263
|
+
// Detect deprecated keys in the raw special sub-object before discarding.
|
|
264
|
+
const rawSpecial = toRecord(record.special);
|
|
265
|
+
for (const key of DEPRECATED_SPECIAL_KEYS) {
|
|
266
|
+
if (key in rawSpecial) {
|
|
267
|
+
configIssues.push(
|
|
268
|
+
`special.${key} is deprecated and ignored — remove it from your policy file.`,
|
|
269
|
+
);
|
|
270
|
+
// Ensure the key is stripped even if its value was a valid PermissionState.
|
|
271
|
+
if (normalized.special) {
|
|
272
|
+
delete normalized.special[key];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
253
277
|
for (const [key, value] of Object.entries(record)) {
|
|
254
278
|
if (!isPermissionState(value)) {
|
|
255
279
|
continue;
|
|
@@ -265,7 +289,7 @@ function normalizeRawPermission(raw: unknown): AgentPermissions {
|
|
|
265
289
|
}
|
|
266
290
|
}
|
|
267
291
|
|
|
268
|
-
return normalized;
|
|
292
|
+
return { permissions: normalized, configIssues };
|
|
269
293
|
}
|
|
270
294
|
|
|
271
295
|
function parseQualifiedMcpToolName(
|
|
@@ -522,6 +546,7 @@ export class PermissionManager {
|
|
|
522
546
|
private configuredMcpServerNamesCache: FileCacheEntry<
|
|
523
547
|
readonly string[]
|
|
524
548
|
> | null = null;
|
|
549
|
+
private accumulatedConfigIssues: string[] = [];
|
|
525
550
|
|
|
526
551
|
constructor(
|
|
527
552
|
options: {
|
|
@@ -554,6 +579,20 @@ export class PermissionManager {
|
|
|
554
579
|
: null;
|
|
555
580
|
}
|
|
556
581
|
|
|
582
|
+
private accumulateConfigIssues(issues: string[]): void {
|
|
583
|
+
for (const issue of issues) {
|
|
584
|
+
if (!this.accumulatedConfigIssues.includes(issue)) {
|
|
585
|
+
this.accumulatedConfigIssues.push(issue);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
getConfigIssues(agentName?: string): string[] {
|
|
591
|
+
// Trigger a load/resolve to ensure issues are collected.
|
|
592
|
+
this.resolvePermissions(agentName);
|
|
593
|
+
return [...this.accumulatedConfigIssues];
|
|
594
|
+
}
|
|
595
|
+
|
|
557
596
|
private loadGlobalConfig(): GlobalPermissionConfig {
|
|
558
597
|
const stamp = getFileStamp(this.globalConfigPath);
|
|
559
598
|
if (this.globalConfigCache?.stamp === stamp) {
|
|
@@ -564,7 +603,9 @@ export class PermissionManager {
|
|
|
564
603
|
try {
|
|
565
604
|
const raw = readFileSync(this.globalConfigPath, "utf-8");
|
|
566
605
|
const parsed = JSON.parse(stripJsonComments(raw)) as unknown;
|
|
567
|
-
const normalized =
|
|
606
|
+
const { permissions: normalized, configIssues } =
|
|
607
|
+
normalizeRawPermission(parsed);
|
|
608
|
+
this.accumulateConfigIssues(configIssues);
|
|
568
609
|
|
|
569
610
|
value = {
|
|
570
611
|
defaultPolicy: normalizePolicy(normalized.defaultPolicy),
|
|
@@ -596,7 +637,9 @@ export class PermissionManager {
|
|
|
596
637
|
try {
|
|
597
638
|
const raw = readFileSync(this.projectGlobalConfigPath, "utf-8");
|
|
598
639
|
const parsed = JSON.parse(stripJsonComments(raw)) as unknown;
|
|
599
|
-
|
|
640
|
+
const result = normalizeRawPermission(parsed);
|
|
641
|
+
value = result.permissions;
|
|
642
|
+
this.accumulateConfigIssues(result.configIssues);
|
|
600
643
|
} catch {
|
|
601
644
|
value = {};
|
|
602
645
|
}
|
|
@@ -629,7 +672,9 @@ export class PermissionManager {
|
|
|
629
672
|
value = {};
|
|
630
673
|
} else {
|
|
631
674
|
const parsed = parseSimpleYamlMap(frontmatter);
|
|
632
|
-
|
|
675
|
+
const result = normalizeRawPermission(parsed.permission);
|
|
676
|
+
value = result.permissions;
|
|
677
|
+
this.accumulateConfigIssues(result.configIssues);
|
|
633
678
|
}
|
|
634
679
|
} catch {
|
|
635
680
|
value = {};
|
|
@@ -32,7 +32,10 @@ import {
|
|
|
32
32
|
SUBAGENT_ENV_HINT_KEYS,
|
|
33
33
|
SUBAGENT_PARENT_SESSION_ENV_KEY,
|
|
34
34
|
} from "../src/permission-forwarding.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
normalizeRawPermission,
|
|
37
|
+
PermissionManager,
|
|
38
|
+
} from "../src/permission-manager.js";
|
|
36
39
|
import {
|
|
37
40
|
findSkillPathMatch,
|
|
38
41
|
parseAllSkillPromptSections,
|
|
@@ -44,7 +47,11 @@ import {
|
|
|
44
47
|
checkRequestedToolRegistration,
|
|
45
48
|
getToolNameFromValue,
|
|
46
49
|
} from "../src/tool-registry.js";
|
|
47
|
-
import type {
|
|
50
|
+
import type {
|
|
51
|
+
AgentPermissions,
|
|
52
|
+
GlobalPermissionConfig,
|
|
53
|
+
PermissionState,
|
|
54
|
+
} from "../src/types.js";
|
|
48
55
|
import {
|
|
49
56
|
canResolveAskPermissionRequest,
|
|
50
57
|
shouldAutoApprovePermissionState,
|
|
@@ -2357,3 +2364,83 @@ test("getResolvedPolicyPaths returns false for missing files and null for absent
|
|
|
2357
2364
|
rmSync(tempDir, { recursive: true, force: true });
|
|
2358
2365
|
}
|
|
2359
2366
|
});
|
|
2367
|
+
|
|
2368
|
+
// --- tool_call_limit deprecation tests (#18) ---
|
|
2369
|
+
|
|
2370
|
+
test("normalizeRawPermission emits deprecation issue for special.tool_call_limit (integer)", () => {
|
|
2371
|
+
const result = normalizeRawPermission({ special: { tool_call_limit: 5 } });
|
|
2372
|
+
assert.equal(result.configIssues.length, 1);
|
|
2373
|
+
assert.ok(result.configIssues[0].includes("tool_call_limit"));
|
|
2374
|
+
assert.equal(result.permissions.special?.tool_call_limit, undefined);
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
test("normalizeRawPermission emits deprecation issue for special.tool_call_limit (string)", () => {
|
|
2378
|
+
const result = normalizeRawPermission({
|
|
2379
|
+
special: { tool_call_limit: "allow" },
|
|
2380
|
+
});
|
|
2381
|
+
assert.equal(result.configIssues.length, 1);
|
|
2382
|
+
assert.ok(result.configIssues[0].includes("tool_call_limit"));
|
|
2383
|
+
assert.equal(result.permissions.special?.tool_call_limit, undefined);
|
|
2384
|
+
});
|
|
2385
|
+
|
|
2386
|
+
test("normalizeRawPermission emits no issues for valid special keys", () => {
|
|
2387
|
+
const result = normalizeRawPermission({
|
|
2388
|
+
special: { doom_loop: "deny" },
|
|
2389
|
+
});
|
|
2390
|
+
assert.equal(result.configIssues.length, 0);
|
|
2391
|
+
assert.equal(result.permissions.special?.doom_loop, "deny");
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
test("normalizeRawPermission emits no issues when special is absent", () => {
|
|
2395
|
+
const result = normalizeRawPermission({ tools: { read: "allow" } });
|
|
2396
|
+
assert.equal(result.configIssues.length, 0);
|
|
2397
|
+
});
|
|
2398
|
+
|
|
2399
|
+
test("PermissionManager.getConfigIssues returns deprecation for tool_call_limit in global config", () => {
|
|
2400
|
+
const config: GlobalPermissionConfig = {
|
|
2401
|
+
defaultPolicy: {
|
|
2402
|
+
tools: "ask",
|
|
2403
|
+
bash: "ask",
|
|
2404
|
+
mcp: "ask",
|
|
2405
|
+
skills: "ask",
|
|
2406
|
+
special: "ask",
|
|
2407
|
+
},
|
|
2408
|
+
tools: {},
|
|
2409
|
+
bash: {},
|
|
2410
|
+
mcp: {},
|
|
2411
|
+
skills: {},
|
|
2412
|
+
special: { tool_call_limit: "allow" as PermissionState, doom_loop: "deny" },
|
|
2413
|
+
};
|
|
2414
|
+
const { manager, cleanup } = createManager(config);
|
|
2415
|
+
try {
|
|
2416
|
+
const issues = manager.getConfigIssues();
|
|
2417
|
+
assert.equal(issues.length, 1);
|
|
2418
|
+
assert.ok(issues[0].includes("tool_call_limit"));
|
|
2419
|
+
} finally {
|
|
2420
|
+
cleanup();
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2423
|
+
|
|
2424
|
+
test("PermissionManager.getConfigIssues returns empty array for clean config", () => {
|
|
2425
|
+
const config: GlobalPermissionConfig = {
|
|
2426
|
+
defaultPolicy: {
|
|
2427
|
+
tools: "ask",
|
|
2428
|
+
bash: "ask",
|
|
2429
|
+
mcp: "ask",
|
|
2430
|
+
skills: "ask",
|
|
2431
|
+
special: "ask",
|
|
2432
|
+
},
|
|
2433
|
+
tools: {},
|
|
2434
|
+
bash: {},
|
|
2435
|
+
mcp: {},
|
|
2436
|
+
skills: {},
|
|
2437
|
+
special: { doom_loop: "deny" },
|
|
2438
|
+
};
|
|
2439
|
+
const { manager, cleanup } = createManager(config);
|
|
2440
|
+
try {
|
|
2441
|
+
const issues = manager.getConfigIssues();
|
|
2442
|
+
assert.equal(issues.length, 0);
|
|
2443
|
+
} finally {
|
|
2444
|
+
cleanup();
|
|
2445
|
+
}
|
|
2446
|
+
});
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Api,
|
|
3
|
-
type AssistantMessageEventStream,
|
|
4
|
-
getApiProvider,
|
|
5
|
-
type Context as LlmContext,
|
|
6
|
-
type Model,
|
|
7
|
-
type SimpleStreamOptions,
|
|
8
|
-
} from "@mariozechner/pi-ai";
|
|
9
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
-
|
|
11
|
-
const GUARDED_TEMPERATURE_APIS = [
|
|
12
|
-
"openai-codex-responses",
|
|
13
|
-
"openai-responses",
|
|
14
|
-
"azure-openai-responses",
|
|
15
|
-
] as const satisfies readonly Api[];
|
|
16
|
-
const OPENAI_RESPONSES_APIS = new Set<Api>([
|
|
17
|
-
"openai-responses",
|
|
18
|
-
"azure-openai-responses",
|
|
19
|
-
]);
|
|
20
|
-
const TEMPERATURE_UNSUPPORTED_APIS = new Set<Api>(["openai-codex-responses"]);
|
|
21
|
-
const TEMPERATURE_UNSUPPORTED_PROVIDERS = new Set<string>(["openai-codex"]);
|
|
22
|
-
|
|
23
|
-
export type ApiStreamSimpleDelegate = (
|
|
24
|
-
model: Model<Api>,
|
|
25
|
-
context: LlmContext,
|
|
26
|
-
options?: SimpleStreamOptions,
|
|
27
|
-
) => AssistantMessageEventStream;
|
|
28
|
-
|
|
29
|
-
type GlobalWithPermissionSystemProviderGuard = typeof globalThis & {
|
|
30
|
-
__piPermissionSystemModelOptionBaseStreams?: Map<
|
|
31
|
-
string,
|
|
32
|
-
ApiStreamSimpleDelegate
|
|
33
|
-
>;
|
|
34
|
-
__piPermissionSystemModelOptionGuardedApis?: Set<string>;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
function getBaseApiStreams(): Map<string, ApiStreamSimpleDelegate> {
|
|
38
|
-
const globalScope = globalThis as GlobalWithPermissionSystemProviderGuard;
|
|
39
|
-
if (!globalScope.__piPermissionSystemModelOptionBaseStreams) {
|
|
40
|
-
globalScope.__piPermissionSystemModelOptionBaseStreams = new Map<
|
|
41
|
-
string,
|
|
42
|
-
ApiStreamSimpleDelegate
|
|
43
|
-
>();
|
|
44
|
-
}
|
|
45
|
-
return globalScope.__piPermissionSystemModelOptionBaseStreams;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getGuardedApis(): Set<string> {
|
|
49
|
-
const globalScope = globalThis as GlobalWithPermissionSystemProviderGuard;
|
|
50
|
-
if (!globalScope.__piPermissionSystemModelOptionGuardedApis) {
|
|
51
|
-
globalScope.__piPermissionSystemModelOptionGuardedApis = new Set<string>();
|
|
52
|
-
}
|
|
53
|
-
return globalScope.__piPermissionSystemModelOptionGuardedApis;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function normalizeIdentifier(value: string | undefined): string {
|
|
57
|
-
return (value ?? "").trim().toLowerCase();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function hasModelToken(modelId: string, token: string): boolean {
|
|
61
|
-
return normalizeIdentifier(modelId)
|
|
62
|
-
.split(/[^a-z0-9]+/)
|
|
63
|
-
.includes(token);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function getUnsupportedTemperatureReason(
|
|
67
|
-
model: Pick<Model<Api>, "api" | "id" | "provider" | "reasoning">,
|
|
68
|
-
): string | undefined {
|
|
69
|
-
if (TEMPERATURE_UNSUPPORTED_APIS.has(model.api)) {
|
|
70
|
-
return `api '${model.api}' does not support temperature`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const provider = normalizeIdentifier(model.provider);
|
|
74
|
-
if (TEMPERATURE_UNSUPPORTED_PROVIDERS.has(provider)) {
|
|
75
|
-
return `provider '${model.provider}' does not support temperature`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
OPENAI_RESPONSES_APIS.has(model.api) &&
|
|
80
|
-
hasModelToken(model.id, "codex")
|
|
81
|
-
) {
|
|
82
|
-
return `model '${model.id}' does not support temperature`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (OPENAI_RESPONSES_APIS.has(model.api) && model.reasoning) {
|
|
86
|
-
return `reasoning model '${model.id}' accepts only the provider default temperature`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
93
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function stripUnsupportedTemperatureFromPayload(
|
|
97
|
-
payload: unknown,
|
|
98
|
-
): unknown {
|
|
99
|
-
if (!isRecord(payload) || !("temperature" in payload)) {
|
|
100
|
-
return payload;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const { temperature: _temperature, ...rest } = payload;
|
|
104
|
-
return rest;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function composeTemperatureSanitizer(
|
|
108
|
-
options: SimpleStreamOptions | undefined,
|
|
109
|
-
model: Model<Api>,
|
|
110
|
-
): SimpleStreamOptions | undefined {
|
|
111
|
-
const reason = getUnsupportedTemperatureReason(model);
|
|
112
|
-
if (!reason && options?.temperature === undefined) {
|
|
113
|
-
return options;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!reason) {
|
|
117
|
-
return options;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const existingOnPayload = options?.onPayload;
|
|
121
|
-
const nextOptions: SimpleStreamOptions = options
|
|
122
|
-
? { ...options, temperature: undefined }
|
|
123
|
-
: {};
|
|
124
|
-
|
|
125
|
-
nextOptions.onPayload = async (payload, payloadModel) => {
|
|
126
|
-
const transformedPayload = existingOnPayload
|
|
127
|
-
? await existingOnPayload(payload, payloadModel)
|
|
128
|
-
: undefined;
|
|
129
|
-
return stripUnsupportedTemperatureFromPayload(
|
|
130
|
-
transformedPayload ?? payload,
|
|
131
|
-
);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
return nextOptions;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function ensureModelOptionGuardForApi(pi: ExtensionAPI, api: Api): boolean {
|
|
138
|
-
const guardedApis = getGuardedApis();
|
|
139
|
-
if (guardedApis.has(api)) {
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const baseStreams = getBaseApiStreams();
|
|
144
|
-
let baseStream = baseStreams.get(api);
|
|
145
|
-
if (!baseStream) {
|
|
146
|
-
const currentProvider = getApiProvider(api);
|
|
147
|
-
if (!currentProvider) {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
baseStream = currentProvider.streamSimple as ApiStreamSimpleDelegate;
|
|
151
|
-
baseStreams.set(api, baseStream);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const providerName = `pi-permission-system-model-option-compatibility-${api.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`;
|
|
155
|
-
pi.registerProvider(providerName, {
|
|
156
|
-
api,
|
|
157
|
-
streamSimple: (model, context, options) => {
|
|
158
|
-
const typedModel = model as Model<Api>;
|
|
159
|
-
const delegate = baseStreams.get(typedModel.api);
|
|
160
|
-
if (!delegate) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
`No base stream provider available for api '${typedModel.api}'.`,
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return delegate(
|
|
167
|
-
typedModel,
|
|
168
|
-
context,
|
|
169
|
-
composeTemperatureSanitizer(options, typedModel),
|
|
170
|
-
);
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
guardedApis.add(api);
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function registerModelOptionCompatibilityGuard(pi: ExtensionAPI): void {
|
|
179
|
-
for (const api of GUARDED_TEMPERATURE_APIS) {
|
|
180
|
-
ensureModelOptionGuardForApi(pi, api);
|
|
181
|
-
}
|
|
182
|
-
}
|