@coseung2/opencodex 2.8.0-cs.15 → 2.8.0-cs.17
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/gui/dist/assets/{index-BhXIu7c0.js → index-Ch-99jy3.js} +2 -2
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/packages/ocx-notch/README.md +3 -1
- package/src/adapters/base.ts +12 -0
- package/src/adapters/identity.ts +1 -1
- package/src/adapters/kiro-calibration.ts +83 -0
- package/src/adapters/kiro-constants.ts +11 -2
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +12 -3
- package/src/adapters/kiro.ts +300 -78
- package/src/adapters/openai-chat.ts +1 -42
- package/src/adapters/openai-responses.ts +126 -9
- package/src/adapters/xai-schema-analysis.ts +78 -0
- package/src/adapters/xai-tool-schema.ts +274 -0
- package/src/adapters/xai-web-search.ts +138 -0
- package/src/bridge.ts +61 -6
- package/src/cli/observe.ts +18 -3
- package/src/codex/app-server-processes.ts +3 -5
- package/src/codex/catalog/effort.ts +4 -2
- package/src/codex/catalog/metadata.ts +42 -9
- package/src/codex/catalog/parsing.ts +17 -2
- package/src/codex/catalog/provider-fetch.ts +9 -3
- package/src/codex/catalog/sync.ts +11 -5
- package/src/codex/data/upstream-models.json +169 -0
- package/src/grok/inject.ts +1 -1
- package/src/lib/errors.ts +18 -0
- package/src/lib/token-estimate.ts +42 -38
- package/src/lib/translator-budget.ts +34 -0
- package/src/oauth/index.ts +10 -4
- package/src/oauth/kiro.ts +71 -6
- package/src/oauth/store.ts +3 -1
- package/src/oauth/types.ts +4 -0
- package/src/providers/derive.ts +7 -5
- package/src/providers/opencode-go-transport.ts +59 -0
- package/src/providers/quota.ts +68 -60
- package/src/providers/registry.ts +41 -10
- package/src/providers/xai-transport.ts +10 -0
- package/src/responses/compaction.ts +8 -1
- package/src/responses/namespace-aliases.ts +56 -0
- package/src/responses/parser.ts +12 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/snapshot-policy.ts +108 -0
- package/src/responses/state.ts +23 -10
- package/src/responses/turn-termination.ts +108 -0
- package/src/responses/xai-custom-tool-compat.ts +237 -0
- package/src/server/grok-responses-snapshot-repair.ts +338 -0
- package/src/server/index.ts +2 -1
- package/src/server/relay-eager.ts +1 -0
- package/src/server/request-log-conversation.ts +8 -0
- package/src/server/request-log.ts +5 -4
- package/src/server/responses/core.ts +233 -16
- package/src/server/responses-image-gen-repair.ts +2 -2
- package/src/server/sse-payload-rewrite.ts +20 -3
- package/src/types.ts +10 -1
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +7 -0
- package/src/usage/log.ts +1 -2
- package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
package/src/oauth/kiro.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type KiroImportDiagnostic,
|
|
28
28
|
} from "./kiro-credentials";
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
|
+
import { KIRO_BUILDER_ID_SERVICE_PROFILE_ARN } from "../adapters/kiro-constants";
|
|
30
31
|
import { getAccountSet, saveAccountCredential } from "./store";
|
|
31
32
|
|
|
32
33
|
const DEFAULT_REGION = "us-east-1";
|
|
@@ -172,13 +173,37 @@ async function defaultKiroCliRunner(args: string[], signal?: AbortSignal): Promi
|
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
|
|
176
|
+
/** Kiro profile ARN structure: arn:<partition>:codewhisperer:<region>:<account>:profile/<id> */
|
|
177
|
+
const KIRO_PROFILE_ARN_PATTERN = /^arn:[a-z0-9-]+:codewhisperer:[a-z0-9-]+:\d{12}:profile\/[A-Za-z0-9-]+$/;
|
|
178
|
+
const KIRO_PROFILE_ARN_MAX_LENGTH = 256;
|
|
179
|
+
|
|
180
|
+
function parseKiroProfileArn(value: unknown): string | undefined {
|
|
181
|
+
if (typeof value !== "string") return undefined;
|
|
182
|
+
const trimmed = value.trim();
|
|
183
|
+
if (trimmed.length === 0 || trimmed.length > KIRO_PROFILE_ARN_MAX_LENGTH) return undefined;
|
|
184
|
+
return KIRO_PROFILE_ARN_PATTERN.test(trimmed) ? trimmed : undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function profileArnFromWhoami(parsed: Record<string, unknown>): string | undefined {
|
|
188
|
+
// Only narrowly-named documented-ish shapes; never invent an ARN (#993).
|
|
189
|
+
return parseKiroProfileArn(parsed.profileArn)
|
|
190
|
+
?? parseKiroProfileArn(parsed.profile_arn)
|
|
191
|
+
?? (parsed.profile && typeof parsed.profile === "object" && !Array.isArray(parsed.profile)
|
|
192
|
+
? parseKiroProfileArn((parsed.profile as Record<string, unknown>).arn)
|
|
193
|
+
: undefined);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function readKiroCliIdentity(runner: KiroCliRunner, signal?: AbortSignal): Promise<{ email?: string; profileArn?: string }> {
|
|
176
197
|
try {
|
|
177
198
|
const result = await runner(["whoami", "--format", "json"], signal);
|
|
178
199
|
if (result.exitCode !== 0) return {};
|
|
179
|
-
const parsed = JSON.parse(result.stdout) as
|
|
200
|
+
const parsed = JSON.parse(result.stdout) as Record<string, unknown>;
|
|
180
201
|
const email = typeof parsed.email === "string" ? parsed.email.trim().toLowerCase() : "";
|
|
181
|
-
|
|
202
|
+
const profileArn = profileArnFromWhoami(parsed);
|
|
203
|
+
return {
|
|
204
|
+
...(email && email.length <= 320 ? { email } : {}),
|
|
205
|
+
...(profileArn ? { profileArn } : {}),
|
|
206
|
+
};
|
|
182
207
|
} catch {
|
|
183
208
|
return {};
|
|
184
209
|
}
|
|
@@ -191,6 +216,7 @@ function metadataFromImported(imported: ImportedKiroCredential): KiroOAuthMetada
|
|
|
191
216
|
...(imported.apiRegion ? { apiRegion: imported.apiRegion } : {}),
|
|
192
217
|
...(imported.clientId ? { clientId: imported.clientId } : {}),
|
|
193
218
|
...(imported.clientSecret ? { clientSecret: imported.clientSecret } : {}),
|
|
219
|
+
...(imported.authType ? { authType: imported.authType } : {}),
|
|
194
220
|
};
|
|
195
221
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
196
222
|
}
|
|
@@ -251,14 +277,34 @@ async function oauthCredentialFromImported(
|
|
|
251
277
|
runner: KiroCliRunner,
|
|
252
278
|
signal?: AbortSignal,
|
|
253
279
|
): Promise<OAuthCredentials> {
|
|
254
|
-
|
|
255
|
-
|
|
280
|
+
let identity: { email?: string; profileArn?: string } = {};
|
|
281
|
+
if (imported.source === "sqlite") {
|
|
282
|
+
identity = await readKiroCliIdentity(runner, signal);
|
|
283
|
+
// Session-switch race (#993 review): another process may have switched the
|
|
284
|
+
// active Kiro CLI session between the SQLite read and whoami. Accept
|
|
285
|
+
// whoami's identity only when the session token STILL matches the import —
|
|
286
|
+
// refresh token, or access token when refresh is absent.
|
|
287
|
+
if (identity.profileArn !== undefined || identity.email !== undefined) {
|
|
288
|
+
const current = readKiroCliSqliteCredential();
|
|
289
|
+
const importedKey = imported.refresh || imported.access;
|
|
290
|
+
const currentKey = current ? current.refresh || current.access : "";
|
|
291
|
+
if (!current || currentKey !== importedKey) identity = {};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Builder ID imports often lack a profileArn in SQLite; whoami against the
|
|
295
|
+
// SAME active CLI session can supply it (#993). Imported stays authoritative.
|
|
296
|
+
const resolvedProfileArn = imported.profileArn ?? identity.profileArn;
|
|
297
|
+
const metadata: KiroOAuthMetadata | undefined = (() => {
|
|
298
|
+
const base = metadataFromImported(imported) ?? {};
|
|
299
|
+
if (resolvedProfileArn && !base.profileArn) base.profileArn = resolvedProfileArn;
|
|
300
|
+
return Object.keys(base).length > 0 ? base : undefined;
|
|
301
|
+
})();
|
|
256
302
|
return {
|
|
257
303
|
access: imported.access,
|
|
258
304
|
refresh: imported.refresh,
|
|
259
305
|
expires: imported.expires,
|
|
260
306
|
source: imported.source === "json" ? "credential-file" : "local-cli",
|
|
261
|
-
...(
|
|
307
|
+
...(resolvedProfileArn ? { accountId: resolvedProfileArn } : {}),
|
|
262
308
|
...(identity.email ? { email: identity.email } : {}),
|
|
263
309
|
...(metadata ? { kiro: metadata } : {}),
|
|
264
310
|
};
|
|
@@ -429,6 +475,25 @@ export function resolveKiroProfileArn(account?: Pick<KiroOAuthMetadata, "profile
|
|
|
429
475
|
return readImportedKiroCredential()?.profileArn;
|
|
430
476
|
}
|
|
431
477
|
|
|
478
|
+
/** Request profile and envelope choice are one decision; a fallback is never account identity. */
|
|
479
|
+
export function resolveKiroRequestProfile(
|
|
480
|
+
account?: Pick<KiroOAuthMetadata, "profileArn" | "authType">,
|
|
481
|
+
): { profileArn: string | undefined; builderIdFallback: boolean } {
|
|
482
|
+
const own = resolveKiroProfileArn(account);
|
|
483
|
+
if (own) return { profileArn: own, builderIdFallback: false };
|
|
484
|
+
// An explicitly selected account must never borrow a different active local CLI account.
|
|
485
|
+
const authType = account === undefined ? readImportedKiroCredential()?.authType : account.authType;
|
|
486
|
+
return authType === "aws_sso_oidc"
|
|
487
|
+
? { profileArn: KIRO_BUILDER_ID_SERVICE_PROFILE_ARN, builderIdFallback: true }
|
|
488
|
+
: { profileArn: undefined, builderIdFallback: false };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function resolveKiroRequestProfileArn(
|
|
492
|
+
account?: Pick<KiroOAuthMetadata, "profileArn" | "authType">,
|
|
493
|
+
): string | undefined {
|
|
494
|
+
return resolveKiroRequestProfile(account).profileArn;
|
|
495
|
+
}
|
|
496
|
+
|
|
432
497
|
async function kiroTokenRefreshError(response: Response): Promise<KiroTokenRefreshError> {
|
|
433
498
|
let oauthError: string | undefined;
|
|
434
499
|
try {
|
package/src/oauth/store.ts
CHANGED
|
@@ -247,13 +247,15 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null {
|
|
|
247
247
|
const apiRegion = clean(kiro.apiRegion, 64);
|
|
248
248
|
const clientId = clean(kiro.clientId, 4096);
|
|
249
249
|
const clientSecret = clean(kiro.clientSecret, 4096);
|
|
250
|
-
|
|
250
|
+
const authType = kiro.authType === "aws_sso_oidc" || kiro.authType === "kiro_desktop" ? kiro.authType : undefined;
|
|
251
|
+
if (profileArn || ssoRegion || apiRegion || clientId || clientSecret || authType) {
|
|
251
252
|
normalized.kiro = {
|
|
252
253
|
...(profileArn ? { profileArn } : {}),
|
|
253
254
|
...(ssoRegion ? { ssoRegion } : {}),
|
|
254
255
|
...(apiRegion ? { apiRegion } : {}),
|
|
255
256
|
...(clientId ? { clientId } : {}),
|
|
256
257
|
...(clientSecret ? { clientSecret } : {}),
|
|
258
|
+
...(authType ? { authType } : {}),
|
|
257
259
|
};
|
|
258
260
|
}
|
|
259
261
|
}
|
package/src/oauth/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Minimal OAuth types, ported from jawcode packages/ai/src/utils/oauth/types.ts. */
|
|
2
2
|
export type OAuthCredentialSource = "oauth" | "local-cli" | "credential-file" | "environment" | "manual";
|
|
3
3
|
|
|
4
|
+
export type KiroCredentialAuthType = "kiro_desktop" | "aws_sso_oidc";
|
|
5
|
+
|
|
4
6
|
/** Account-scoped Kiro data required for refresh and request routing. */
|
|
5
7
|
export interface KiroOAuthMetadata {
|
|
6
8
|
profileArn?: string;
|
|
@@ -8,6 +10,8 @@ export interface KiroOAuthMetadata {
|
|
|
8
10
|
apiRegion?: string;
|
|
9
11
|
clientId?: string;
|
|
10
12
|
clientSecret?: string;
|
|
13
|
+
/** Non-secret request-routing signal; never substitute for an account profile. */
|
|
14
|
+
authType?: KiroCredentialAuthType;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export type OAuthCredentials = {
|
package/src/providers/derive.ts
CHANGED
|
@@ -239,15 +239,17 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
239
239
|
if (!prov.models && seed.models) prov.models = [...seed.models];
|
|
240
240
|
if (prov.liveModels === undefined && seed.liveModels !== undefined) prov.liveModels = seed.liveModels;
|
|
241
241
|
if (prov.contextWindow === undefined && seed.contextWindow !== undefined) prov.contextWindow = seed.contextWindow;
|
|
242
|
-
if (
|
|
242
|
+
if (seed.modelContextWindows) prov.modelContextWindows = { ...seed.modelContextWindows, ...prov.modelContextWindows };
|
|
243
243
|
if (seed.modelInputModalities) prov.modelInputModalities = fillRecordOfArrays(seed.modelInputModalities, prov.modelInputModalities);
|
|
244
244
|
if (prov.defaultMaxOutputTokens === undefined && seed.defaultMaxOutputTokens !== undefined) prov.defaultMaxOutputTokens = seed.defaultMaxOutputTokens;
|
|
245
|
-
if (
|
|
245
|
+
if (seed.modelMaxOutputTokens) prov.modelMaxOutputTokens = { ...seed.modelMaxOutputTokens, ...prov.modelMaxOutputTokens };
|
|
246
246
|
if (!prov.reasoningEfforts && seed.reasoningEfforts) prov.reasoningEfforts = [...seed.reasoningEfforts];
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
// One customized model must not hide the registry's knowledge of every other
|
|
248
|
+
// model. Fill per key on new maps; explicit empty ladders remain authoritative.
|
|
249
|
+
if (seed.modelReasoningEfforts) prov.modelReasoningEfforts = fillRecordOfArrays(seed.modelReasoningEfforts, prov.modelReasoningEfforts);
|
|
250
|
+
if (seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts, ...prov.modelDefaultReasoningEfforts };
|
|
249
251
|
if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
|
|
250
|
-
if (
|
|
252
|
+
if (seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = { ...cloneNestedRecord(seed.modelReasoningEffortMap), ...cloneNestedRecord(prov.modelReasoningEffortMap ?? {}) };
|
|
251
253
|
if (!prov.noVisionModels && seed.noVisionModels) prov.noVisionModels = [...seed.noVisionModels];
|
|
252
254
|
if (!prov.noReasoningModels && seed.noReasoningModels) prov.noReasoningModels = [...seed.noReasoningModels];
|
|
253
255
|
if (!prov.noTemperatureModels && seed.noTemperatureModels) prov.noTemperatureModels = [...seed.noTemperatureModels];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { OcxProviderConfig } from "../types";
|
|
3
|
+
import { registryEntryForProviderDestination } from "./registry";
|
|
4
|
+
|
|
5
|
+
export const OPENCODE_GO_SESSION_HEADER = "x-opencode-session";
|
|
6
|
+
|
|
7
|
+
const MUSE_RESPONSE_MODELS = new Set(["muse-spark-1.2-contributor", "muse-spark-1.3-contributor"]);
|
|
8
|
+
const MUSE_RESPONSE_URLS = new Set([
|
|
9
|
+
"https://opencode.ai/zen/v1/responses",
|
|
10
|
+
"https://opencode.ai/zen/go/v1/responses",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/** Compatibility belongs to the exact model AND effective destination, not a provider label. */
|
|
14
|
+
export function isOpenCodeMuseResponses(modelId: unknown, responseUrl: string): boolean {
|
|
15
|
+
if (typeof modelId !== "string" || !MUSE_RESPONSE_MODELS.has(modelId.trim().toLowerCase())) return false;
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(responseUrl);
|
|
18
|
+
if (url.username || url.password || url.search || url.hash) return false;
|
|
19
|
+
return MUSE_RESPONSE_URLS.has(`${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasHeaderCaseInsensitive(
|
|
26
|
+
headers: Record<string, string> | undefined,
|
|
27
|
+
name: string,
|
|
28
|
+
): boolean {
|
|
29
|
+
const target = name.toLowerCase();
|
|
30
|
+
return Object.keys(headers ?? {}).some(key => key.toLowerCase() === target);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Derive a provider-scoped opaque value without exposing Codex task or subagent ids. */
|
|
34
|
+
export function deriveOpenCodeGoSessionId(sessionLane: string): string {
|
|
35
|
+
const digest = createHash("sha256")
|
|
36
|
+
.update("opencodex/opencode-go/session/v1\0")
|
|
37
|
+
.update(sessionLane)
|
|
38
|
+
.digest("hex")
|
|
39
|
+
.slice(0, 32);
|
|
40
|
+
return `ocx_${digest}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Add per-conversation Go affinity only to the canonical fixed-key destination. */
|
|
44
|
+
export function resolveOpenCodeGoTransport<T extends OcxProviderConfig>(
|
|
45
|
+
provider: T,
|
|
46
|
+
sessionLane: string | undefined,
|
|
47
|
+
): T {
|
|
48
|
+
if (registryEntryForProviderDestination(provider)?.id !== "opencode-go") return provider;
|
|
49
|
+
if (!sessionLane) return provider;
|
|
50
|
+
if (hasHeaderCaseInsensitive(provider.headers, OPENCODE_GO_SESSION_HEADER)) return provider;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...provider,
|
|
54
|
+
headers: {
|
|
55
|
+
...(provider.headers ?? {}),
|
|
56
|
+
[OPENCODE_GO_SESSION_HEADER]: deriveOpenCodeGoSessionId(sessionLane),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/providers/quota.ts
CHANGED
|
@@ -992,6 +992,22 @@ function opencodeGoSegments(api: NonNullable<Awaited<ReturnType<typeof fetchOpen
|
|
|
992
992
|
return segments;
|
|
993
993
|
}
|
|
994
994
|
|
|
995
|
+
function opencodeGoQuotaFromSegments(
|
|
996
|
+
segments: NonNullable<ProviderQuotaWindow["segments"]>,
|
|
997
|
+
now: number,
|
|
998
|
+
): ProviderQuota | null {
|
|
999
|
+
if (segments.length === 0) return null;
|
|
1000
|
+
return {
|
|
1001
|
+
customWindows: [{
|
|
1002
|
+
// Row label intentionally empty: the segments carry their own labels.
|
|
1003
|
+
label: "",
|
|
1004
|
+
percent: 0,
|
|
1005
|
+
segments,
|
|
1006
|
+
}],
|
|
1007
|
+
updatedAt: now,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
995
1011
|
/**
|
|
996
1012
|
* opencode.go allocation: prefer the key-scoped usage endpoint (exact console
|
|
997
1013
|
* percents + real reset times); fall back to the local request-count estimate
|
|
@@ -1001,18 +1017,8 @@ async function fetchOpencodeGoQuota(name: string, config: OcxProviderConfig): Pr
|
|
|
1001
1017
|
const activeKey = resolveEnvValue(config.apiKey)?.trim() ?? config.apiKey;
|
|
1002
1018
|
const api = await fetchOpencodeGoUsageApi(activeKey).catch(() => null);
|
|
1003
1019
|
if (api) {
|
|
1004
|
-
const
|
|
1005
|
-
if (
|
|
1006
|
-
return report(name, "opencode-go:usage-api", {
|
|
1007
|
-
customWindows: [{
|
|
1008
|
-
// Row label intentionally empty: the segments carry their own labels.
|
|
1009
|
-
label: "",
|
|
1010
|
-
percent: 0,
|
|
1011
|
-
segments,
|
|
1012
|
-
}],
|
|
1013
|
-
updatedAt: Date.now(),
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1020
|
+
const quota = opencodeGoQuotaFromSegments(opencodeGoSegments(api), Date.now());
|
|
1021
|
+
if (quota) return report(name, "opencode-go:usage-api", quota);
|
|
1016
1022
|
}
|
|
1017
1023
|
|
|
1018
1024
|
// Fallback: dominant model's local request counts against published limits.
|
|
@@ -1023,36 +1029,30 @@ async function fetchOpencodeGoQuota(name: string, config: OcxProviderConfig): Pr
|
|
|
1023
1029
|
if (!dominant) return null;
|
|
1024
1030
|
const { fiveHour, weekly, monthly } = OPENCODE_GO_LIMITS[dominant]!;
|
|
1025
1031
|
const now = Date.now();
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
label: "",
|
|
1029
|
-
percent: 0,
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
resetAt: now + OPENCODE_GO_COST_WINDOW_MS,
|
|
1045
|
-
},
|
|
1046
|
-
],
|
|
1047
|
-
}],
|
|
1048
|
-
updatedAt: now,
|
|
1049
|
-
});
|
|
1032
|
+
const quota = opencodeGoQuotaFromSegments([
|
|
1033
|
+
{
|
|
1034
|
+
label: "5h",
|
|
1035
|
+
percent: normalizePercent(((estimate.fiveHourCounts.get(dominant) ?? 0) / fiveHour) * 100) ?? 0,
|
|
1036
|
+
resetAt: now + OPENCODE_GO_FIVE_HOUR_MS,
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
label: "Weekly",
|
|
1040
|
+
percent: normalizePercent(((estimate.weeklyCounts.get(dominant) ?? 0) / weekly) * 100) ?? 0,
|
|
1041
|
+
resetAt: now + OPENCODE_GO_WEEK_MS,
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
label: "Monthly",
|
|
1045
|
+
percent: normalizePercent(((estimate.monthlyCounts.get(dominant) ?? 0) / monthly) * 100) ?? 0,
|
|
1046
|
+
resetAt: now + OPENCODE_GO_COST_WINDOW_MS,
|
|
1047
|
+
},
|
|
1048
|
+
], now);
|
|
1049
|
+
return quota ? report(name, "opencode-go:docs-estimate", quota) : null;
|
|
1050
1050
|
}
|
|
1051
1051
|
|
|
1052
1052
|
/**
|
|
1053
|
-
* Per-key monthly
|
|
1054
|
-
*
|
|
1055
|
-
* endpoint rejects fall back to the local
|
|
1053
|
+
* Per-key 5h/weekly/monthly allocation for every connected key. The usage
|
|
1054
|
+
* endpoint is key-scoped, so each pool key reports its own live windows. Keys
|
|
1055
|
+
* the endpoint rejects fall back to the local request-count estimate.
|
|
1056
1056
|
*/
|
|
1057
1057
|
export async function opencodeGoKeyQuotaEstimates(config: OcxConfig, name: string): Promise<Record<string, ProviderQuota> | null> {
|
|
1058
1058
|
const provider = config.providers[name];
|
|
@@ -1064,40 +1064,48 @@ export async function opencodeGoKeyQuotaEstimates(config: OcxConfig, name: strin
|
|
|
1064
1064
|
const now = Date.now();
|
|
1065
1065
|
const out: Record<string, ProviderQuota> = {};
|
|
1066
1066
|
const pool = provider.apiKeyPool ?? [];
|
|
1067
|
+
const activeKey = resolveEnvValue(provider.apiKey)?.trim() ?? provider.apiKey;
|
|
1068
|
+
const activeKeyId = activeKey ? pool.find(entry => entry.key === activeKey)?.id : undefined;
|
|
1067
1069
|
if (pool.length > 0) {
|
|
1068
1070
|
const results = await Promise.all(pool.map(async entry => {
|
|
1069
1071
|
const api = await fetchOpencodeGoUsageApi(entry.key).catch(() => null);
|
|
1070
|
-
return [entry.id, api
|
|
1072
|
+
return [entry.id, api] as const;
|
|
1071
1073
|
}));
|
|
1072
|
-
for (const [keyId,
|
|
1073
|
-
if (
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
label: "월간 할당",
|
|
1077
|
-
percent: monthly.percent,
|
|
1078
|
-
...(monthly.resetAt !== undefined ? { resetAt: monthly.resetAt } : {}),
|
|
1079
|
-
}],
|
|
1080
|
-
updatedAt: now,
|
|
1081
|
-
};
|
|
1074
|
+
for (const [keyId, api] of results) {
|
|
1075
|
+
if (!api) continue;
|
|
1076
|
+
const quota = opencodeGoQuotaFromSegments(opencodeGoSegments(api), now);
|
|
1077
|
+
if (quota) out[keyId] = quota;
|
|
1082
1078
|
}
|
|
1083
1079
|
}
|
|
1084
1080
|
// Fallback for keys the endpoint did not answer.
|
|
1085
1081
|
const estimate = estimateOpencodeGoUsage(name, provider);
|
|
1086
1082
|
if (estimate) {
|
|
1087
1083
|
const dominant = [...estimate.monthlyCounts.entries()]
|
|
1088
|
-
.sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
1084
|
+
.sort((a, b) => b[1] - a[1] || (estimate.weeklyCounts.get(b[0]) ?? 0) - (estimate.weeklyCounts.get(a[0]) ?? 0))[0]?.[0];
|
|
1089
1085
|
if (dominant) {
|
|
1090
|
-
const
|
|
1086
|
+
const limits = OPENCODE_GO_LIMITS[dominant]!;
|
|
1091
1087
|
for (const [keyId, count] of estimate.perKeyMonthlyCounts) {
|
|
1092
1088
|
if (out[keyId]) continue;
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1089
|
+
const segments: NonNullable<ProviderQuotaWindow["segments"]> = [];
|
|
1090
|
+
if (keyId === activeKeyId) {
|
|
1091
|
+
segments.push({
|
|
1092
|
+
label: "5h",
|
|
1093
|
+
percent: normalizePercent(((estimate.fiveHourCounts.get(dominant) ?? 0) / limits.fiveHour) * 100) ?? 0,
|
|
1094
|
+
resetAt: now + OPENCODE_GO_FIVE_HOUR_MS,
|
|
1095
|
+
});
|
|
1096
|
+
segments.push({
|
|
1097
|
+
label: "Weekly",
|
|
1098
|
+
percent: normalizePercent(((estimate.weeklyCounts.get(dominant) ?? 0) / limits.weekly) * 100) ?? 0,
|
|
1099
|
+
resetAt: now + OPENCODE_GO_WEEK_MS,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
segments.push({
|
|
1103
|
+
label: "Monthly",
|
|
1104
|
+
percent: normalizePercent((count / limits.monthly) * 100) ?? 0,
|
|
1105
|
+
resetAt: now + OPENCODE_GO_COST_WINDOW_MS,
|
|
1106
|
+
});
|
|
1107
|
+
const quota = opencodeGoQuotaFromSegments(segments, now);
|
|
1108
|
+
if (quota) out[keyId] = quota;
|
|
1101
1109
|
}
|
|
1102
1110
|
}
|
|
1103
1111
|
}
|
|
@@ -29,7 +29,12 @@ export type InboundWire = "responses" | "chat" | "anthropic";
|
|
|
29
29
|
* A per-model wire default: a bare string applies to every inbound, while the object
|
|
30
30
|
* form applies only to the listed inbound protocols.
|
|
31
31
|
*/
|
|
32
|
-
export type ModelWireDefault = string | {
|
|
32
|
+
export type ModelWireDefault = string | {
|
|
33
|
+
wire: string;
|
|
34
|
+
inbound: readonly InboundWire[];
|
|
35
|
+
/** Optional auth-mode gate for providers whose subscription and API-key products use different wires. */
|
|
36
|
+
authModes?: readonly ProviderAuthKind[];
|
|
37
|
+
};
|
|
33
38
|
|
|
34
39
|
export type ProviderModelDiscoveryScalar = string | number | boolean;
|
|
35
40
|
|
|
@@ -700,6 +705,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
700
705
|
// transport returns 400 ("Multi Agent requests are not allowed on chat completions").
|
|
701
706
|
models: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
|
|
702
707
|
defaultModel: "grok-4.5",
|
|
708
|
+
// Grok's subscription gateway exposes 4.6/4.5 natively on Responses. Scope the default to
|
|
709
|
+
// OAuth Codex traffic: API-key and Chat/Anthropic callers keep their existing wire, while an
|
|
710
|
+
// explicit modelAdapters override still wins over this registry-only default.
|
|
711
|
+
modelWireDefaults: {
|
|
712
|
+
"grok-4.6": { wire: "openai-responses", inbound: ["responses"], authModes: ["oauth"] },
|
|
713
|
+
"grok-4.5": { wire: "openai-responses", inbound: ["responses"], authModes: ["oauth"] },
|
|
714
|
+
},
|
|
703
715
|
// Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
|
|
704
716
|
// models accept image input (JPEG/PNG, URL or base64). Without this the catalog leaves
|
|
705
717
|
// inputModalities undefined, and deriveComboCatalogModel defaults an undefined member to
|
|
@@ -808,6 +820,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
808
820
|
// catalog authoritative so a spurious 2xx from runtime.../models cannot drop seeded ids
|
|
809
821
|
// (e.g. newly listed GPT-5.6 tiers) via live-discovery reconciliation.
|
|
810
822
|
liveModels: false,
|
|
823
|
+
// Kiro rejects request-level parallel tool calls; keep persisted presets and the Codex
|
|
824
|
+
// catalog aligned with the adapter's single-call capability guard.
|
|
825
|
+
parallelToolCalls: false,
|
|
811
826
|
// Per-model context metadata is maintained next to the Kiro model list.
|
|
812
827
|
modelContextWindows: KIRO_MODEL_CONTEXT_WINDOWS,
|
|
813
828
|
modelReasoningEfforts: KIRO_MODEL_REASONING_EFFORTS,
|
|
@@ -821,16 +836,20 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
821
836
|
featured: true,
|
|
822
837
|
dashboardUrl: "https://platform.openai.com/api-keys",
|
|
823
838
|
defaultModel: "gpt-5.5",
|
|
824
|
-
models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS],
|
|
839
|
+
models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, "gpt-6-astra"],
|
|
825
840
|
liveModels: true,
|
|
826
|
-
|
|
827
|
-
|
|
841
|
+
// API limits differ from the Codex-login Astra pin. Keep the public API ladder
|
|
842
|
+
// separate: low..max is documented, while the native catalog also carries ultra.
|
|
843
|
+
modelContextWindows: { ...OPENAI_API_GPT56_CONTEXT_WINDOWS, "gpt-6-astra": 1_050_000 },
|
|
844
|
+
modelMaxInputTokens: { ...OPENAI_API_GPT56_MAX_INPUT_TOKENS, "gpt-6-astra": 922_000 },
|
|
845
|
+
modelMaxOutputTokens: { "gpt-6-astra": 128_000 },
|
|
828
846
|
modelInputModalities: Object.fromEntries(
|
|
829
|
-
["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, ["text", "image"]]),
|
|
830
|
-
),
|
|
831
|
-
modelReasoningEfforts: Object.fromEntries(
|
|
832
|
-
[...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_REASONING_EFFORTS]),
|
|
847
|
+
["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, "gpt-6-astra"].map(id => [id, ["text", "image"]]),
|
|
833
848
|
),
|
|
849
|
+
modelReasoningEfforts: {
|
|
850
|
+
...Object.fromEntries([...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_REASONING_EFFORTS])),
|
|
851
|
+
"gpt-6-astra": ["low", "medium", "high", "xhigh", "max"],
|
|
852
|
+
},
|
|
834
853
|
virtualModels: OPENAI_API_GPT56_VIRTUAL_MODELS,
|
|
835
854
|
},
|
|
836
855
|
{
|
|
@@ -862,13 +881,21 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
862
881
|
id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1",
|
|
863
882
|
authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
|
|
864
883
|
jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…",
|
|
884
|
+
modelWireDefaults: {
|
|
885
|
+
"muse-spark-1.3-contributor": "openai-responses",
|
|
886
|
+
"muse-spark-1.2-contributor": "openai-responses",
|
|
887
|
+
},
|
|
865
888
|
modelContextWindows: {
|
|
866
889
|
"kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW,
|
|
890
|
+
"muse-spark-1.3-contributor": 1_048_576,
|
|
891
|
+
"muse-spark-1.2-contributor": 1_048_576,
|
|
867
892
|
[OPENCODE_OX_ALPHA_FREE_MODEL]: OX_ALPHA_CONTEXT_WINDOW,
|
|
868
893
|
[DEEPSEEK_VISION_PREVIEW_MODEL]: 1_048_576,
|
|
869
894
|
},
|
|
870
895
|
modelInputModalities: {
|
|
871
896
|
"kimi-k3": ["text", "image"],
|
|
897
|
+
"muse-spark-1.3-contributor": ["text", "image"],
|
|
898
|
+
"muse-spark-1.2-contributor": ["text", "image"],
|
|
872
899
|
[OPENCODE_OX_ALPHA_FREE_MODEL]: ["text", "image"],
|
|
873
900
|
[DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"],
|
|
874
901
|
},
|
|
@@ -1792,8 +1819,12 @@ export function providerModelWireDefault(
|
|
|
1792
1819
|
if (!entry?.modelWireDefaults || !providerMatchesRegistryTransport(id, provider)) return undefined;
|
|
1793
1820
|
const declared = entry.modelWireDefaults[modelId.trim().toLowerCase()];
|
|
1794
1821
|
if (declared === undefined) return undefined;
|
|
1795
|
-
// A bare string applies to every inbound; the object form
|
|
1796
|
-
if (typeof declared !== "string"
|
|
1822
|
+
// A bare string applies to every inbound/auth mode; the object form may narrow either.
|
|
1823
|
+
if (typeof declared !== "string") {
|
|
1824
|
+
if (!declared.inbound.includes(inbound)) return undefined;
|
|
1825
|
+
const authMode = provider.authMode ?? entry.authKind;
|
|
1826
|
+
if (declared.authModes && !declared.authModes.includes(authMode)) return undefined;
|
|
1827
|
+
}
|
|
1797
1828
|
const wire = typeof declared === "string" ? declared : declared.wire;
|
|
1798
1829
|
return wire !== undefined && allowedWires.has(wire) ? wire : undefined;
|
|
1799
1830
|
}
|
|
@@ -22,6 +22,16 @@ export const XAI_GROK_COMPATIBILITY = {
|
|
|
22
22
|
export const XAI_GROK_CLIENT_VERSION = XAI_GROK_COMPATIBILITY.version;
|
|
23
23
|
export const XAI_CONV_ID_HEADER = XAI_GROK_COMPATIBILITY.headers.conversationId;
|
|
24
24
|
|
|
25
|
+
/** Both xAI Responses hosts share the same request dialect. */
|
|
26
|
+
export function isXaiResponsesDestination(provider: Pick<OcxProviderConfig, "baseUrl">): boolean {
|
|
27
|
+
try {
|
|
28
|
+
const hostname = new URL(provider.baseUrl).hostname.toLowerCase();
|
|
29
|
+
return hostname === "api.x.ai" || hostname === "cli-chat-proxy.grok.com";
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
export type OcxProviderTransport = OcxProviderConfig & {
|
|
26
36
|
/** Request executor used only at runtime; never persisted. */
|
|
27
37
|
fetch?: typeof globalThis.fetch;
|
|
@@ -105,7 +105,14 @@ export function buildCompactV1Output(userMessages: string[], summary: string): R
|
|
|
105
105
|
remaining -= msg.length;
|
|
106
106
|
} else {
|
|
107
107
|
// Budget partially covers this older message: keep its tail (most recent context) and stop.
|
|
108
|
-
|
|
108
|
+
let tailStart = msg.length - remaining;
|
|
109
|
+
// Never start the retained tail on a lone LOW surrogate: the pair's
|
|
110
|
+
// other half would be lost and encoding substitutes U+FFFD.
|
|
111
|
+
if (tailStart > 0 && tailStart < msg.length) {
|
|
112
|
+
const first = msg.charCodeAt(tailStart);
|
|
113
|
+
if (first >= 0xdc00 && first <= 0xdfff) tailStart += 1;
|
|
114
|
+
}
|
|
115
|
+
selected.push(msg.slice(tailStart));
|
|
109
116
|
break;
|
|
110
117
|
}
|
|
111
118
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { namespacedToolName, type OcxTool } from "../types";
|
|
2
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
3
|
+
|
|
4
|
+
export interface NamespacedToolIdentity { namespace: string; name: string }
|
|
5
|
+
type ToolIdentity = Pick<OcxTool, "namespace" | "name">;
|
|
6
|
+
|
|
7
|
+
function sameIdentity(left: ToolIdentity, right: ToolIdentity): boolean {
|
|
8
|
+
return (left.namespace ?? "") === (right.namespace ?? "") && left.name === right.name;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve all ownership before publishing aliases. Dots may occur in both halves,
|
|
13
|
+
* and a flat declaration can own a spelling which looks namespaced. Neither the
|
|
14
|
+
* declaration order nor an ambiguous dotted name may choose a different tool.
|
|
15
|
+
*/
|
|
16
|
+
export function declaredNamespaceAliases(
|
|
17
|
+
tools: readonly ToolIdentity[],
|
|
18
|
+
budget?: TranslatorBudget,
|
|
19
|
+
): Map<string, NamespacedToolIdentity> {
|
|
20
|
+
const owners = new Map<string, ToolIdentity | null>();
|
|
21
|
+
const candidates = new Set<string>();
|
|
22
|
+
let temporaryBytes = 0;
|
|
23
|
+
const claim = (spelling: string, tool: ToolIdentity, candidate: boolean) => {
|
|
24
|
+
if (!owners.has(spelling)) {
|
|
25
|
+
const bytes = Buffer.byteLength(spelling) + Buffer.byteLength(tool.name)
|
|
26
|
+
+ Buffer.byteLength(tool.namespace ?? "") + 64;
|
|
27
|
+
budget?.chargeRetained(bytes, { kind: "request_copies" });
|
|
28
|
+
temporaryBytes += bytes;
|
|
29
|
+
owners.set(spelling, tool);
|
|
30
|
+
} else {
|
|
31
|
+
const owner = owners.get(spelling);
|
|
32
|
+
if (owner && !sameIdentity(owner, tool)) owners.set(spelling, null);
|
|
33
|
+
}
|
|
34
|
+
if (candidate) candidates.add(spelling);
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
for (const tool of tools) claim(namespacedToolName(tool.namespace, tool.name), tool, !!tool.namespace);
|
|
38
|
+
for (const tool of tools) {
|
|
39
|
+
if (!tool.namespace || tool.namespace.includes("__") || tool.name.includes("__")) continue;
|
|
40
|
+
claim(`${tool.namespace}.${tool.name}`, tool, true);
|
|
41
|
+
}
|
|
42
|
+
const aliases = new Map<string, NamespacedToolIdentity>();
|
|
43
|
+
for (const spelling of candidates) {
|
|
44
|
+
const owner = owners.get(spelling);
|
|
45
|
+
if (!owner?.namespace) continue;
|
|
46
|
+
budget?.chargeRetained(
|
|
47
|
+
Buffer.byteLength(spelling) + Buffer.byteLength(owner.namespace) + Buffer.byteLength(owner.name),
|
|
48
|
+
{ kind: "request_copies" },
|
|
49
|
+
);
|
|
50
|
+
aliases.set(spelling, { namespace: owner.namespace, name: owner.name });
|
|
51
|
+
}
|
|
52
|
+
return aliases;
|
|
53
|
+
} finally {
|
|
54
|
+
budget?.releaseRetained(temporaryBytes, { kind: "request_copies" });
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/responses/parser.ts
CHANGED
|
@@ -425,6 +425,18 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
425
425
|
: null;
|
|
426
426
|
const thinkingText = envelope?.txt || text;
|
|
427
427
|
|
|
428
|
+
// Kiro reasoning round-trip: a krc-only item carries nothing renderable — it is provider
|
|
429
|
+
// state for the assistant turn that ALREADY closed, because Kiro emits its
|
|
430
|
+
// reasoningContentEvent at the END of a turn (after content AND tool calls, verified
|
|
431
|
+
// against kiro-cli 2.14.1/2.16.0). Folding it into the FOLLOWING turn like ordinary
|
|
432
|
+
// reasoning would attach turn N's blob to turn N+1, so attach it backwards instead. With
|
|
433
|
+
// no assistant turn to own it the blob is dropped rather than mis-paired.
|
|
434
|
+
if (envelope?.krc && thinkingText.length === 0) {
|
|
435
|
+
const previous = messages[messages.length - 1];
|
|
436
|
+
if (previous?.role === "assistant") previous.kiroRedactedReasoning = envelope.krc;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
428
440
|
// Native/non-ocxr1 encrypted-only reasoning is opaque here. Do not create a detached
|
|
429
441
|
// assistant turn or invent replayable plaintext/signatures from the encrypted payload.
|
|
430
442
|
if (thinkingText.length > 0) {
|
|
@@ -24,6 +24,12 @@ export interface ReasoningEnvelope {
|
|
|
24
24
|
* so replay needs it even though the visible summary was suppressed.
|
|
25
25
|
*/
|
|
26
26
|
txt?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Kiro `reasoningContentEvent.redactedContent`: a KMS-encrypted reasoning blob that is opaque to
|
|
29
|
+
* the proxy. Kiro's own CLI replays it on the matching `assistantResponseMessage` to preserve
|
|
30
|
+
* model reasoning across turns, so it round-trips here the same way a signature does.
|
|
31
|
+
*/
|
|
32
|
+
krc?: string;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export function encodeReasoningEnvelope(envelope: ReasoningEnvelope): string {
|
|
@@ -45,7 +51,9 @@ export function decodeReasoningEnvelope(encryptedContent: string): ReasoningEnve
|
|
|
45
51
|
}
|
|
46
52
|
const txt = (parsed as { txt?: unknown }).txt;
|
|
47
53
|
if (typeof txt === "string" && txt.length > 0) envelope.txt = txt;
|
|
48
|
-
|
|
54
|
+
const krc = (parsed as { krc?: unknown }).krc;
|
|
55
|
+
if (typeof krc === "string" && krc.length > 0) envelope.krc = krc;
|
|
56
|
+
return envelope.sig || envelope.red || envelope.txt || envelope.krc ? envelope : null;
|
|
49
57
|
} catch {
|
|
50
58
|
return null;
|
|
51
59
|
}
|