@oxygen-agent/cli 1.1010.721 → 1.1010.905

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.
Files changed (100) hide show
  1. package/README.md +1 -1
  2. package/dist/auto-update.d.ts +129 -0
  3. package/dist/auto-update.js +392 -0
  4. package/dist/command-manifest.js +14 -0
  5. package/dist/credentials.d.ts +2 -0
  6. package/dist/credentials.js +6 -3
  7. package/dist/functions-commands.js +1 -1
  8. package/dist/http-client.js +28 -4
  9. package/dist/index.js +583 -145
  10. package/dist/run-wait.d.ts +3 -1
  11. package/dist/run-wait.js +19 -5
  12. package/dist/streamed-file-import.d.ts +58 -0
  13. package/dist/streamed-file-import.js +115 -0
  14. package/dist/update.d.ts +29 -0
  15. package/dist/update.js +62 -16
  16. package/dist/workflow-plan-limit-notices.d.ts +8 -0
  17. package/dist/workflow-plan-limit-notices.js +28 -0
  18. package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
  19. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +17 -0
  20. package/node_modules/@oxygen/shared/dist/billing-anchors.js +27 -0
  21. package/node_modules/@oxygen/shared/dist/billing.d.ts +191 -35
  22. package/node_modules/@oxygen/shared/dist/billing.js +333 -42
  23. package/node_modules/@oxygen/shared/dist/capability-discovery.js +55 -5
  24. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  25. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  26. package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
  27. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
  28. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
  29. package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
  30. package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
  31. package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
  32. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
  33. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
  34. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +3 -1
  35. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +3 -3
  36. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
  37. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
  38. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
  39. package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
  40. package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
  41. package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
  42. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
  43. package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
  44. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
  45. package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
  46. package/node_modules/@oxygen/shared/dist/index.d.ts +2 -22
  47. package/node_modules/@oxygen/shared/dist/index.js +2 -42
  48. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
  49. package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
  50. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
  51. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
  52. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +117 -1
  53. package/node_modules/@oxygen/shared/dist/plan-band.js +175 -10
  54. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
  55. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
  56. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
  57. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
  58. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +204 -6
  59. package/node_modules/@oxygen/shared/dist/plan-limits.js +197 -15
  60. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +80 -36
  61. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +80 -31
  62. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +38 -20
  63. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +47 -34
  64. package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
  65. package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
  66. package/node_modules/@oxygen/shared/dist/repricing.d.ts +127 -0
  67. package/node_modules/@oxygen/shared/dist/repricing.js +407 -6
  68. package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
  69. package/node_modules/@oxygen/shared/dist/semver.js +41 -0
  70. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +5 -7
  71. package/node_modules/@oxygen/shared/dist/sending-limits.js +10 -16
  72. package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
  73. package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
  74. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
  75. package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
  76. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +15 -7
  77. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +18 -5
  78. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +34 -5
  79. package/node_modules/@oxygen/shared/dist/table-capacity.js +25 -8
  80. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
  81. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
  82. package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
  83. package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
  84. package/node_modules/@oxygen/shared/dist/telemetry.js +5 -0
  85. package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
  86. package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
  87. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
  88. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  89. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  90. package/node_modules/@oxygen/shared/dist/version.js +14 -27
  91. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
  92. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
  93. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
  94. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
  95. package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
  96. package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
  97. package/node_modules/@oxygen/workflows/dist/index.js +152 -2
  98. package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
  99. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
  100. package/package.json +1 -1
@@ -76,6 +76,10 @@ export type FeatureGateResolution = {
76
76
  * (`packages/shared/src/repricing.ts`). A remotely flippable value would let
77
77
  * a mis-click move every customer's prices; it changes only through Doppler,
78
78
  * with explicit human approval.
79
+ * - OXYGEN_CREDIT_GATE_ENABLED — (b). Lets a connect through on the balance
80
+ * instead of a sending seat (`packages/shared/src/credit-gate.ts`). A wrong
81
+ * "on" while commitments do not bill every connectable kind would admit
82
+ * accounts nothing ever charges.
79
83
  *
80
84
  * The list is a guard, not a to-do: none of these is migrated, and
81
85
  * `findFeatureGateRegistryViolations` fails the build's tests if one ever is.
@@ -67,6 +67,10 @@
67
67
  * (`packages/shared/src/repricing.ts`). A remotely flippable value would let
68
68
  * a mis-click move every customer's prices; it changes only through Doppler,
69
69
  * with explicit human approval.
70
+ * - OXYGEN_CREDIT_GATE_ENABLED — (b). Lets a connect through on the balance
71
+ * instead of a sending seat (`packages/shared/src/credit-gate.ts`). A wrong
72
+ * "on" while commitments do not bill every connectable kind would admit
73
+ * accounts nothing ever charges.
70
74
  *
71
75
  * The list is a guard, not a to-do: none of these is migrated, and
72
76
  * `findFeatureGateRegistryViolations` fails the build's tests if one ever is.
@@ -82,6 +86,7 @@ export const NEVER_FLAGGABLE = [
82
86
  "OXYGEN_TRIGGER_DEFAULT_CAPS",
83
87
  "OXYGEN_BYOK_DAILY_CAPS",
84
88
  "OXYGEN_REPRICING_2026_09_EFFECTIVE_AT",
89
+ "OXYGEN_CREDIT_GATE_ENABLED",
85
90
  ];
86
91
  /**
87
92
  * Every server-side gate resolved through PostHog.
@@ -1,6 +1,6 @@
1
1
  import { type Row } from "read-excel-file/node";
2
2
  import { type ImportColumnDataType } from "./column-types.js";
3
- export { MAX_BUFFERED_IMPORT_PARSE_BYTES } from "./import-limits.js";
3
+ export { IMPORT_OBJECT_SINGLE_PUT_MAX_BYTES, MAX_BUFFERED_IMPORT_PARSE_BYTES, MAX_URL_IMPORT_BYTES, bufferedImportTooLargeMessage, isBufferedOnlyImportFormat, } from "./import-limits.js";
4
4
  export type RowsFileFormat = "json" | "jsonl" | "csv" | "xlsx";
5
5
  export type ImportTableColumn = {
6
6
  label: string;
@@ -68,3 +68,15 @@ export declare function parseRowsSampleText(text: string, format: Exclude<RowsFi
68
68
  export declare function inferImportColumnLabels(rows: Record<string, unknown>[]): string[];
69
69
  export declare function normalizeRowsForNewTable(rows: Record<string, unknown>[]): NewTableImportRows;
70
70
  export declare function normalizeImportColumnKey(value: string): string;
71
+ /**
72
+ * Refuse a JSON or XLSX file over the buffered-parse ceiling before anything is
73
+ * uploaded or queued. CSV and JSONL stream, so they pass at any size and only
74
+ * the plan's file limit applies to them.
75
+ */
76
+ export declare function assertImportFormatWithinBufferedLimit(format: RowsFileFormat, fileBytes: number): void;
77
+ /**
78
+ * Refuse a file sent whole inside one request (the multipart fallback used when
79
+ * object storage is not configured) over the buffered ceiling: that path holds
80
+ * the file in the web process, whatever the plan's file limit says.
81
+ */
82
+ export declare function assertInlineImportWithinLimit(fileBytes: number): void;
@@ -3,8 +3,8 @@ import readXlsxFile from "read-excel-file/node";
3
3
  import { inferImportColumnDataType, parseDateValueToIso, } from "./column-types.js";
4
4
  import { OxygenError } from "./cli-result.js";
5
5
  import { makeUniqueIdentifier, toSnakeIdentifier } from "./identifiers.js";
6
- import { MAX_BUFFERED_IMPORT_PARSE_BYTES } from "./import-limits.js";
7
- export { MAX_BUFFERED_IMPORT_PARSE_BYTES } from "./import-limits.js";
6
+ import { MAX_BUFFERED_IMPORT_PARSE_BYTES, bufferedImportTooLargeMessage, isBufferedOnlyImportFormat, } from "./import-limits.js";
7
+ export { IMPORT_OBJECT_SINGLE_PUT_MAX_BYTES, MAX_BUFFERED_IMPORT_PARSE_BYTES, MAX_URL_IMPORT_BYTES, bufferedImportTooLargeMessage, isBufferedOnlyImportFormat, } from "./import-limits.js";
8
8
  export function inferRowsFileFormat(path) {
9
9
  const extension = extname(path).toLowerCase();
10
10
  if (extension === ".jsonl" || extension === ".ndjson")
@@ -497,10 +497,11 @@ async function streamToLimitedBuffer(chunks, format) {
497
497
  const buffer = Buffer.from(toUint8Array(chunk));
498
498
  total += buffer.byteLength;
499
499
  if (total > MAX_BUFFERED_IMPORT_PARSE_BYTES) {
500
- throw new OxygenError("import_file_too_large_for_format", `${format.toUpperCase()} imports must be uploaded as CSV or JSONL for large files.`, {
500
+ throw new OxygenError("import_file_too_large_for_format", bufferedImportTooLargeMessage(format), {
501
501
  details: {
502
502
  format,
503
503
  max_buffered_parse_bytes: MAX_BUFFERED_IMPORT_PARSE_BYTES,
504
+ next_step: "Save the file as CSV or JSONL and import it again.",
504
505
  },
505
506
  exitCode: 1,
506
507
  });
@@ -523,13 +524,39 @@ function normalizeBatchSize(value) {
523
524
  return Math.max(1, Math.trunc(value));
524
525
  }
525
526
  function assertBufferedParseWithinLimit(buffer, format) {
526
- if (buffer.byteLength <= MAX_BUFFERED_IMPORT_PARSE_BYTES)
527
+ assertImportFormatWithinBufferedLimit(format, buffer.byteLength);
528
+ }
529
+ /**
530
+ * Refuse a JSON or XLSX file over the buffered-parse ceiling before anything is
531
+ * uploaded or queued. CSV and JSONL stream, so they pass at any size and only
532
+ * the plan's file limit applies to them.
533
+ */
534
+ export function assertImportFormatWithinBufferedLimit(format, fileBytes) {
535
+ if (!isBufferedOnlyImportFormat(format) || fileBytes <= MAX_BUFFERED_IMPORT_PARSE_BYTES)
527
536
  return;
528
- throw new OxygenError("buffered_import_too_large", "Large JSON array and XLSX imports require buffered parsing; use CSV or JSONL for large staged imports.", {
537
+ throw new OxygenError("buffered_import_too_large", bufferedImportTooLargeMessage(format, fileBytes), {
529
538
  details: {
530
539
  format,
531
- file_bytes: buffer.byteLength,
540
+ file_bytes: fileBytes,
532
541
  max_buffered_parse_bytes: MAX_BUFFERED_IMPORT_PARSE_BYTES,
542
+ next_step: "Save the file as CSV or JSONL and import it again.",
543
+ },
544
+ exitCode: 1,
545
+ });
546
+ }
547
+ /**
548
+ * Refuse a file sent whole inside one request (the multipart fallback used when
549
+ * object storage is not configured) over the buffered ceiling: that path holds
550
+ * the file in the web process, whatever the plan's file limit says.
551
+ */
552
+ export function assertInlineImportWithinLimit(fileBytes) {
553
+ if (fileBytes <= MAX_BUFFERED_IMPORT_PARSE_BYTES)
554
+ return;
555
+ throw new OxygenError("request_too_large", "Files this large upload straight to file storage, which is not available for this request.", {
556
+ details: {
557
+ file_bytes: fileBytes,
558
+ max_inline_file_bytes: MAX_BUFFERED_IMPORT_PARSE_BYTES,
559
+ next_step: "Import it with `oxygen tables import --file <path>` or the web import, which stage large files in object storage.",
533
560
  },
534
561
  exitCode: 1,
535
562
  });
@@ -36,7 +36,12 @@ export type CopilotAiLevel = HostedAiLevel | "auto";
36
36
  export type HostedAiModelSpec = {
37
37
  /** OpenRouter model id, e.g. "deepseek/deepseek-v4-flash". */
38
38
  model: string;
39
- /** Human-readable label for dashboards/UI. */
39
+ /**
40
+ * What a customer sees for this tier: always the Oxygen tier label, never the
41
+ * vendor model name ("DO not expose MODELs names to users", Philipp,
42
+ * 2026-09-26). Customer surfaces read this (e.g. the Copilot session's
43
+ * `model_display_name`); staff and logs read `model`.
44
+ */
40
45
  displayName: string;
41
46
  /** Ordered fallback model ids tried when `model` is unavailable. */
42
47
  fallbackModels: readonly string[];
@@ -54,12 +59,29 @@ export type HostedAiModelSpec = {
54
59
  estCompletionUsdPerM: number;
55
60
  /** Upper bound on completion tokens requested for this tier. */
56
61
  maxOutputTokens: number;
62
+ /**
63
+ * Send `reasoning: {enabled: false}` on a managed call to this tier's default
64
+ * model. Only where cost and quality were MEASURED with reasoning off: it is
65
+ * what the tier's price is calibrated against (ai_column Balanced, 2026-09-26).
66
+ */
67
+ disableReasoning?: boolean;
57
68
  };
58
69
  export declare const HOSTED_AI_MODEL_REGISTRY: {
59
70
  ai_column: Record<HostedAiLevel, HostedAiModelSpec>;
60
71
  copilot: Record<CopilotAiLevel, HostedAiModelSpec>;
61
72
  agent: Record<HostedAiLevel, HostedAiModelSpec>;
62
73
  };
74
+ /**
75
+ * Managed AI-column model ids that USED to back a tier, mapped to the tier they
76
+ * billed at. A managed column saved with one of these pinned keeps validating
77
+ * (tenant-db `resolveManagedAiModelTier`) and runs on that tier's CURRENT model,
78
+ * so retiring a model never strands a saved column or keeps it on a model we no
79
+ * longer sell. A BYOK column keeps whatever it pinned: that is the customer's
80
+ * own key and choice. Retired 2026-09-26 (see the ai_column block above).
81
+ */
82
+ export declare const LEGACY_MANAGED_AI_COLUMN_MODELS: Readonly<Record<string, HostedAiLevel>>;
83
+ /** The tier a retired managed AI-column model id billed at, or null. */
84
+ export declare function legacyManagedAiColumnLevel(model: string): HostedAiLevel | null;
63
85
  /**
64
86
  * What a MANAGED AI column's reasoning tier is called in front of a customer.
65
87
  *
@@ -123,8 +145,8 @@ export declare const AGENT_DEFAULT_LEVEL: HostedAiLevel;
123
145
  * Resolve the model spec for a hosted-AI use case + reasoning level.
124
146
  *
125
147
  * For `copilot`, Doppler overrides are applied here:
126
- * - `OXYGEN_COPILOT_MODEL_{LOW,MEDIUM,HIGH}` replaces `.model` (and, since no
127
- * separate label is supplied, `.displayName` falls back to the model id).
148
+ * - `OXYGEN_COPILOT_MODEL_{LOW,MEDIUM,HIGH}` replaces `.model`; `.displayName`
149
+ * keeps the tier label, because a customer never sees a model id.
128
150
  * - `OXYGEN_COPILOT_FALLBACK_{LOW,MEDIUM,HIGH}` replaces `.fallbackModels`
129
151
  * from a comma-separated list.
130
152
  *
@@ -146,6 +168,54 @@ export declare function resolveHostedAiModel(input: {
146
168
  level: CopilotAiLevel;
147
169
  env?: Record<string, string | undefined>;
148
170
  }): HostedAiModelSpec;
171
+ /**
172
+ * The customer-facing label for a model id OXYGEN manages, or null when the id is
173
+ * not one of ours (a BYOK pin, a customer's own request). OpenRouter can answer
174
+ * with a dated or routed variant (`vendor/model-20260915`, `vendor/model:nitro`),
175
+ * so those match their base id.
176
+ */
177
+ export declare function managedModelLabel(model: string, preferredUseCase?: HostedAiUseCase): string | null;
178
+ /**
179
+ * What an AI-column run's provenance records as its model: the tier label for
180
+ * a managed run, and the model itself only when the customer pinned it on
181
+ * their own key. A BYOK run on the tier default is still OXYGEN's pick.
182
+ */
183
+ export declare function customerFacingAiColumnModel(input: {
184
+ model: string;
185
+ credentialMode: "managed" | "byok";
186
+ reasoningLevel: HostedAiLevel;
187
+ }): string;
188
+ /**
189
+ * The model OpenRouter reports it answered with, for the same provenance. A
190
+ * managed run may have been served by a fallback, so it reads as the tier; a
191
+ * customer's own pin (or its dated variant) stays theirs.
192
+ */
193
+ export declare function customerFacingAiColumnResponseModel(input: {
194
+ model: string;
195
+ credentialMode: "managed" | "byok";
196
+ reasoningLevel: HostedAiLevel;
197
+ responseModel: string | null;
198
+ }): string | null;
199
+ /**
200
+ * An AI-column request summary as the customer reads it: its `model` is the
201
+ * tier (or their pin), and any other managed id inside is redacted too.
202
+ */
203
+ export declare function customerFacingAiColumnRequest<T extends Record<string, unknown>>(summary: T, input: {
204
+ model: string;
205
+ credentialMode: "managed" | "byok";
206
+ reasoningLevel: HostedAiLevel;
207
+ }): T;
208
+ /**
209
+ * Replace every string in `value` that is a managed model id with its tier
210
+ * label. Walks objects and arrays; only a string that IS a model id is touched,
211
+ * so prose that merely mentions a model is left alone. `allow` lists ids the
212
+ * customer chose (a BYOK pin), which pass through untouched.
213
+ */
214
+ export declare function redactManagedModelIds<T>(value: T, options?: {
215
+ allow?: readonly (string | null | undefined)[];
216
+ preferredUseCase?: HostedAiUseCase;
217
+ env?: Record<string, string | undefined>;
218
+ }): T;
149
219
  /**
150
220
  * Convenience map of each reasoning tier's default `.model` id for a use case.
151
221
  * `@oxygen/integrations` and `@oxygen/tenant-db` re-source their AI-column
@@ -21,29 +21,87 @@
21
21
  * can re-source the catalog without dragging in the shared barrel.
22
22
  */
23
23
  export const HOSTED_AI_MODEL_REGISTRY = {
24
+ /**
25
+ * Managed AI-column tiers, re-picked 2026-09-26 on measured evidence
26
+ * (Philipp, 2026-09-26: Clay-level quality, faster, 30-50% cheaper than Clay,
27
+ * and at least 5x markup on AI and research columns -- with one explicit
28
+ * founder exception for Max, below).
29
+ *
30
+ * Quality: a blind bakeoff of 13 OpenRouter models x 60 synthetic rows on the
31
+ * four jobs AI columns do (classify, extract, copy, grounded research), every
32
+ * request built by the runner's own buildOpenRouterRequestBody; copy and
33
+ * research graded 1-5 by a blind opus judge (spend $1.74). The previous tiers
34
+ * (DeepSeek V4 Flash / V4 Pro / Kimi K2.6) placed 11th-13th of 13.
35
+ *
36
+ * Cost: a second, production-shaped calibration run (golden-prodshape.json:
37
+ * research with 6-10 sources totalling ~6-20k tokens, copy with ~2.5k tokens
38
+ * of workspace context, extract/classify over ~1.5-3k tokens of scraped text;
39
+ * spend $1.80). Each model's mean cost on that set is taken as a multiple of
40
+ * a reference model whose REAL production mean is known from Langfuse, and
41
+ * that multiple is the projection. Harness: scripts/ops/ai-column-bakeoff
42
+ * (`--golden`, `summarize --reference`).
43
+ *
44
+ * tier model blind overall prod-mean $/row credits markup
45
+ * Fast gpt-6-luna 4.13 (res 4.80) 0.0013 (1.68x v4-flash @ $0.00077) 1.5 ~11x
46
+ * Balanced deepseek-v4.1-flash, no reasoning 4.23 0.0079 (0.60x v4-pro @ $0.0132) 4 ~5.1x
47
+ * Max gpt-6-sol 4.13 (5th; fastest, 1-3 s) 0.098 25 ~2.5x
48
+ *
49
+ * Balanced runs with reasoning OFF (`disableReasoning`): on the prod-shaped
50
+ * set it cost $0.00141/row against reasoning-on v4-pro's $0.00235 with
51
+ * classify, extract and research accuracy unchanged (100%; research citations
52
+ * 90%). With reasoning on it would not clear 5x at 4 credits.
53
+ *
54
+ * Max at ~2.5x is an EXPLICIT founder exception to the 5x rule (Philipp,
55
+ * 2026-09-26: "gpt-6 sol @ 2.5x markup"). Sol answered research correctly on
56
+ * every prod-shaped row and is the fastest of the 13. Two earlier Max picks
57
+ * were dropped the same day on cost: claude-opus-5.5 (#1, 4.63) billed ~1.8x
58
+ * the input tokens for the same rows, and kimi-k3 (#2, 4.37) projects to
59
+ * ~$0.183/row -- about 92 credits at 5x, its copy rows costing 48x v4-pro's.
60
+ * The first token-average projection (Langfuse token profile x list price)
61
+ * undercounted real cost by ~4x, which is why pricing now uses the
62
+ * calibration above. Neither never-shipped id is in the legacy map.
63
+ *
64
+ * Against Clay: fixed content models cost 0.2 Data Credits (GPT-5 Nano), 0.5
65
+ * (Gemini 2.5 Flash) and 1 (GPT-4.1/Helium) at >= $0.05 per Data Credit
66
+ * ($0.074-0.083 on plans) plus ~$0.01 per Action, so Fast and Balanced land
67
+ * well under Clay. Clay passes frontier models through at cost.
68
+ *
69
+ * Each tier falls back to a different vendor through OpenRouter's `models`
70
+ * routing, sent only when the tier default runs (ai-column-runner).
71
+ *
72
+ * The $49 trap, as an aside: one synthetic research row lists the real plan
73
+ * ($199/location/month) beside a $49 dental-school education price. 9 of 13
74
+ * models answered $49, including Luna, V4.1 Flash and Sol; Gemini 3.8 Flash
75
+ * (Max's fallback) was one of the four that did not.
76
+ *
77
+ * `reasoningEffort` is deliberately unset on every tier; the only reasoning
78
+ * control measured for columns is Balanced's off switch above.
79
+ * Retired ids live in LEGACY_MANAGED_AI_COLUMN_MODELS.
80
+ */
24
81
  ai_column: {
25
82
  low: {
26
- model: "deepseek/deepseek-v4-flash",
27
- displayName: "DeepSeek V4 Flash",
28
- fallbackModels: [],
29
- estPromptUsdPerM: 0.09,
30
- estCompletionUsdPerM: 0.18,
83
+ model: "openai/gpt-6-luna",
84
+ displayName: "Oxygen Fast",
85
+ fallbackModels: ["deepseek/deepseek-v4.1-flash"],
86
+ estPromptUsdPerM: 0.1,
87
+ estCompletionUsdPerM: 0.5,
31
88
  maxOutputTokens: 4096,
32
89
  },
33
90
  medium: {
34
- model: "deepseek/deepseek-v4-pro",
35
- displayName: "DeepSeek V4 Pro",
36
- fallbackModels: [],
37
- estPromptUsdPerM: 0.435,
38
- estCompletionUsdPerM: 0.87,
91
+ model: "deepseek/deepseek-v4.1-flash",
92
+ displayName: "Oxygen Balanced",
93
+ fallbackModels: ["openai/gpt-6-luna"],
94
+ estPromptUsdPerM: 0.3,
95
+ estCompletionUsdPerM: 1.2,
39
96
  maxOutputTokens: 4096,
97
+ disableReasoning: true,
40
98
  },
41
99
  high: {
42
- model: "moonshotai/kimi-k2.6",
43
- displayName: "Kimi K2.6",
44
- fallbackModels: [],
45
- estPromptUsdPerM: 0.66,
46
- estCompletionUsdPerM: 3.41,
100
+ model: "openai/gpt-6-sol",
101
+ displayName: "Oxygen Max",
102
+ fallbackModels: ["google/gemini-3.8-flash"],
103
+ estPromptUsdPerM: 2,
104
+ estCompletionUsdPerM: 10,
47
105
  maxOutputTokens: 4096,
48
106
  },
49
107
  },
@@ -104,7 +162,7 @@ export const HOSTED_AI_MODEL_REGISTRY = {
104
162
  // (classifyModelError sends it straight to the next model), so a 20%
105
163
  // error rate here is a 20% double round trip on every delegation.
106
164
  model: "deepseek/deepseek-v4-flash",
107
- displayName: "DeepSeek V4 Flash",
165
+ displayName: "Oxygen Fast",
108
166
  // v4.1-flash earns its place HERE: it works 4 times in 5 and it is a
109
167
  // genuinely different endpoint on a different provider, which is the one
110
168
  // thing a fallback has to be.
@@ -116,7 +174,7 @@ export const HOSTED_AI_MODEL_REGISTRY = {
116
174
  },
117
175
  medium: {
118
176
  model: "deepseek/deepseek-v4-pro",
119
- displayName: "DeepSeek V4 Pro",
177
+ displayName: "Oxygen Balanced",
120
178
  fallbackModels: [],
121
179
  reasoningEffort: "high",
122
180
  estPromptUsdPerM: 0.435,
@@ -132,7 +190,7 @@ export const HOSTED_AI_MODEL_REGISTRY = {
132
190
  */
133
191
  high: {
134
192
  model: "moonshotai/kimi-k3",
135
- displayName: "Kimi K3",
193
+ displayName: "Oxygen Max",
136
194
  fallbackModels: ["z-ai/glm-5.3"],
137
195
  reasoningEffort: "medium",
138
196
  estPromptUsdPerM: 1.7,
@@ -143,7 +201,7 @@ export const HOSTED_AI_MODEL_REGISTRY = {
143
201
  agent: {
144
202
  low: {
145
203
  model: "deepseek/deepseek-v4-flash",
146
- displayName: "DeepSeek V4 Flash",
204
+ displayName: "Oxygen Fast",
147
205
  fallbackModels: [],
148
206
  reasoningEffort: "medium",
149
207
  estPromptUsdPerM: 0.09,
@@ -152,7 +210,7 @@ export const HOSTED_AI_MODEL_REGISTRY = {
152
210
  },
153
211
  medium: {
154
212
  model: "deepseek/deepseek-v4-pro",
155
- displayName: "DeepSeek V4 Pro",
213
+ displayName: "Oxygen Balanced",
156
214
  fallbackModels: ["deepseek/deepseek-v4-flash"],
157
215
  reasoningEffort: "high",
158
216
  estPromptUsdPerM: 0.435,
@@ -161,7 +219,7 @@ export const HOSTED_AI_MODEL_REGISTRY = {
161
219
  },
162
220
  high: {
163
221
  model: "moonshotai/kimi-k2.6",
164
- displayName: "Kimi K2.6",
222
+ displayName: "Oxygen Max",
165
223
  fallbackModels: ["deepseek/deepseek-v4-pro"],
166
224
  estPromptUsdPerM: 0.66,
167
225
  estCompletionUsdPerM: 3.41,
@@ -169,6 +227,25 @@ export const HOSTED_AI_MODEL_REGISTRY = {
169
227
  },
170
228
  },
171
229
  };
230
+ /**
231
+ * Managed AI-column model ids that USED to back a tier, mapped to the tier they
232
+ * billed at. A managed column saved with one of these pinned keeps validating
233
+ * (tenant-db `resolveManagedAiModelTier`) and runs on that tier's CURRENT model,
234
+ * so retiring a model never strands a saved column or keeps it on a model we no
235
+ * longer sell. A BYOK column keeps whatever it pinned: that is the customer's
236
+ * own key and choice. Retired 2026-09-26 (see the ai_column block above).
237
+ */
238
+ export const LEGACY_MANAGED_AI_COLUMN_MODELS = {
239
+ "deepseek/deepseek-v4-flash": "low",
240
+ "deepseek/deepseek-v4-pro": "medium",
241
+ "moonshotai/kimi-k2.6": "high",
242
+ };
243
+ /** The tier a retired managed AI-column model id billed at, or null. */
244
+ export function legacyManagedAiColumnLevel(model) {
245
+ return Object.prototype.hasOwnProperty.call(LEGACY_MANAGED_AI_COLUMN_MODELS, model)
246
+ ? LEGACY_MANAGED_AI_COLUMN_MODELS[model] ?? null
247
+ : null;
248
+ }
172
249
  /**
173
250
  * What a MANAGED AI column's reasoning tier is called in front of a customer.
174
251
  *
@@ -258,8 +335,8 @@ function parseFallbackCsv(raw) {
258
335
  * Resolve the model spec for a hosted-AI use case + reasoning level.
259
336
  *
260
337
  * For `copilot`, Doppler overrides are applied here:
261
- * - `OXYGEN_COPILOT_MODEL_{LOW,MEDIUM,HIGH}` replaces `.model` (and, since no
262
- * separate label is supplied, `.displayName` falls back to the model id).
338
+ * - `OXYGEN_COPILOT_MODEL_{LOW,MEDIUM,HIGH}` replaces `.model`; `.displayName`
339
+ * keeps the tier label, because a customer never sees a model id.
263
340
  * - `OXYGEN_COPILOT_FALLBACK_{LOW,MEDIUM,HIGH}` replaces `.fallbackModels`
264
341
  * from a comma-separated list.
265
342
  *
@@ -311,13 +388,158 @@ export function resolveHostedAiModel(input) {
311
388
  const resolved = { ...base };
312
389
  if (modelOverride !== undefined) {
313
390
  resolved.model = modelOverride;
314
- resolved.displayName = modelOverride;
315
391
  }
316
392
  if (fallbackOverride !== undefined) {
317
393
  resolved.fallbackModels = fallbackOverride;
318
394
  }
319
395
  return resolved;
320
396
  }
397
+ /**
398
+ * "DO not expose MODELs names to users" (Philipp, 2026-09-26).
399
+ *
400
+ * A customer buys an Oxygen tier, so every customer surface names the tier
401
+ * (Oxygen Fast / Balanced / Max / Auto) and never the vendor model OXYGEN picked
402
+ * behind it. The model ids stay in logs, Langfuse and staff surfaces. The only
403
+ * model a customer sees is one they chose themselves: a BYOK pin or a Copilot
404
+ * `--model` request, which callers pass as `allow`.
405
+ *
406
+ * Provenance written before this rule (cell sources, run events, Copilot events)
407
+ * still carries raw ids, so the redaction runs where a payload leaves for a
408
+ * customer rather than where it is written.
409
+ */
410
+ const BACKUP_MODEL_LABEL = "Oxygen backup model";
411
+ // TypeSafe's System One classifier (`SYSTEM_ONE_MODEL` in @oxygen/integrations,
412
+ // pinned as `jev-<version>`) is OXYGEN's choice too. It answers classification
413
+ // columns and inbox triage at the managed Fast price, so provenance written with
414
+ // its id (before the write-time fix, or by a later version bump) reads as that
415
+ // tier. Matched by shape so a version bump stays covered.
416
+ const SYSTEM_ONE_MODEL_ID = /^jev-(?:\d+(?:\.\d+)*|latest)$/;
417
+ const SYSTEM_ONE_MODEL_LABEL = MANAGED_AI_TIER_LABELS.low;
418
+ // Doppler can swap any tier's model without a deploy; the swapped-in id is still
419
+ // OXYGEN's choice, so it is redacted like the registry default it replaces.
420
+ const MODEL_OVERRIDE_ENV_PREFIXES = ["OXYGEN_OPENROUTER_MODEL_", "OXYGEN_COPILOT_MODEL_", "OXYGEN_AGENT_MODEL_"];
421
+ function managedModelLabels(preferredUseCase, env = typeof process === "undefined" ? {} : process.env) {
422
+ const labels = new Map();
423
+ for (const prefix of MODEL_OVERRIDE_ENV_PREFIXES) {
424
+ for (const level of ["LOW", "MEDIUM", "HIGH", "AUTO"]) {
425
+ const model = readTrimmedEnv(env, `${prefix}${level}`);
426
+ if (!model || labels.has(model))
427
+ continue;
428
+ labels.set(model, level === "AUTO" ? COPILOT_TIER_LABELS.auto : MANAGED_AI_TIER_LABELS[level.toLowerCase()]);
429
+ }
430
+ }
431
+ const useCases = Object.keys(HOSTED_AI_MODEL_REGISTRY);
432
+ const ordered = preferredUseCase
433
+ ? [preferredUseCase, ...useCases.filter((useCase) => useCase !== preferredUseCase)]
434
+ : useCases;
435
+ for (const useCase of ordered) {
436
+ for (const spec of Object.values(HOSTED_AI_MODEL_REGISTRY[useCase])) {
437
+ if (!labels.has(spec.model))
438
+ labels.set(spec.model, spec.displayName);
439
+ }
440
+ }
441
+ for (const [model, level] of Object.entries(LEGACY_MANAGED_AI_COLUMN_MODELS)) {
442
+ if (!labels.has(model))
443
+ labels.set(model, MANAGED_AI_TIER_LABELS[level]);
444
+ }
445
+ for (const useCase of ordered) {
446
+ for (const spec of Object.values(HOSTED_AI_MODEL_REGISTRY[useCase])) {
447
+ for (const fallback of spec.fallbackModels) {
448
+ if (!labels.has(fallback))
449
+ labels.set(fallback, BACKUP_MODEL_LABEL);
450
+ }
451
+ }
452
+ }
453
+ return labels;
454
+ }
455
+ /**
456
+ * The customer-facing label for a model id OXYGEN manages, or null when the id is
457
+ * not one of ours (a BYOK pin, a customer's own request). OpenRouter can answer
458
+ * with a dated or routed variant (`vendor/model-20260915`, `vendor/model:nitro`),
459
+ * so those match their base id.
460
+ */
461
+ export function managedModelLabel(model, preferredUseCase) {
462
+ return labelForManagedModel(managedModelLabels(preferredUseCase), model);
463
+ }
464
+ function labelForManagedModel(labels, model) {
465
+ const exact = labels.get(model);
466
+ if (exact)
467
+ return exact;
468
+ for (const [id, label] of labels) {
469
+ if (model.startsWith(`${id}-`) || model.startsWith(`${id}:`))
470
+ return label;
471
+ }
472
+ if (SYSTEM_ONE_MODEL_ID.test(model))
473
+ return SYSTEM_ONE_MODEL_LABEL;
474
+ return null;
475
+ }
476
+ /**
477
+ * What an AI-column run's provenance records as its model: the tier label for
478
+ * a managed run, and the model itself only when the customer pinned it on
479
+ * their own key. A BYOK run on the tier default is still OXYGEN's pick.
480
+ */
481
+ export function customerFacingAiColumnModel(input) {
482
+ if (input.credentialMode === "byok" && managedModelLabel(input.model, "ai_column") === null)
483
+ return input.model;
484
+ return MANAGED_AI_TIER_LABELS[input.reasoningLevel];
485
+ }
486
+ /**
487
+ * The model OpenRouter reports it answered with, for the same provenance. A
488
+ * managed run may have been served by a fallback, so it reads as the tier; a
489
+ * customer's own pin (or its dated variant) stays theirs.
490
+ */
491
+ export function customerFacingAiColumnResponseModel(input) {
492
+ if (input.responseModel === null)
493
+ return null;
494
+ const requested = customerFacingAiColumnModel(input);
495
+ return requested === input.model ? input.responseModel : requested;
496
+ }
497
+ /**
498
+ * An AI-column request summary as the customer reads it: its `model` is the
499
+ * tier (or their pin), and any other managed id inside is redacted too.
500
+ */
501
+ export function customerFacingAiColumnRequest(summary, input) {
502
+ const redacted = redactManagedModelIds(summary, { preferredUseCase: "ai_column" });
503
+ return "model" in summary ? { ...redacted, model: customerFacingAiColumnModel(input) } : redacted;
504
+ }
505
+ /**
506
+ * Replace every string in `value` that is a managed model id with its tier
507
+ * label. Walks objects and arrays; only a string that IS a model id is touched,
508
+ * so prose that merely mentions a model is left alone. `allow` lists ids the
509
+ * customer chose (a BYOK pin), which pass through untouched.
510
+ */
511
+ export function redactManagedModelIds(value, options = {}) {
512
+ const allow = new Set((options.allow ?? []).filter((id) => typeof id === "string" && id.length > 0));
513
+ const labels = managedModelLabels(options.preferredUseCase, options.env);
514
+ const labelFor = (model) => allow.has(model) ? null : labelForManagedModel(labels, model);
515
+ const walk = (node) => {
516
+ if (typeof node === "string")
517
+ return labelFor(node) ?? node;
518
+ if (Array.isArray(node)) {
519
+ let changed = false;
520
+ const next = node.map((item) => {
521
+ const mapped = walk(item);
522
+ if (mapped !== item)
523
+ changed = true;
524
+ return mapped;
525
+ });
526
+ return changed ? next : node;
527
+ }
528
+ if (node && typeof node === "object" && !(node instanceof Date)) {
529
+ let changed = false;
530
+ const next = {};
531
+ for (const [key, item] of Object.entries(node)) {
532
+ const mapped = walk(item);
533
+ if (mapped !== item)
534
+ changed = true;
535
+ next[key] = mapped;
536
+ }
537
+ return changed ? next : node;
538
+ }
539
+ return node;
540
+ };
541
+ return walk(value);
542
+ }
321
543
  /**
322
544
  * Convenience map of each reasoning tier's default `.model` id for a use case.
323
545
  * `@oxygen/integrations` and `@oxygen/tenant-db` re-source their AI-column
@@ -13,9 +13,27 @@
13
13
  /**
14
14
  * Formats that must be held in memory to parse at all — a JSON array has no
15
15
  * record boundary to stream from, and XLSX is a zip container. CSV and JSONL
16
- * stream, so only these two are capped this low.
16
+ * stream from object storage up to the plan's file limit, so only these two
17
+ * carry this lower ceiling (raised from 10 MiB by decision F.8, 2026-09-27).
18
+ *
19
+ * It also bounds every path that holds a whole file in one request: the inline
20
+ * multipart import used when object storage is not configured.
17
21
  */
18
22
  export declare const MAX_BUFFERED_IMPORT_PARSE_BYTES: number;
23
+ /**
24
+ * A link import (`--url`, `oxygen_tables_import_url`, the wizard's link card)
25
+ * is downloaded and parsed inside the web request, so it keeps its own small
26
+ * ceiling. It is not a plan limit: a bigger file is downloaded and imported
27
+ * with `tables import --file`, which stages it in object storage.
28
+ */
29
+ export declare const MAX_URL_IMPORT_BYTES: number;
30
+ /**
31
+ * The largest object one presigned PUT may carry on an S3-compatible store (S3
32
+ * and Ceph-based stores such as Hetzner Object Storage cap a single PUT at
33
+ * 5 GiB). A staged import is one PUT, so no plan's import file limit may
34
+ * exceed it.
35
+ */
36
+ export declare const IMPORT_OBJECT_SINGLE_PUT_MAX_BYTES: number;
19
37
  /**
20
38
  * The platform's request-body ceiling — infrastructure, not a plan limit.
21
39
  *
@@ -26,3 +44,9 @@ export declare const MAX_BUFFERED_IMPORT_PARSE_BYTES: number;
26
44
  export declare const VERCEL_REQUEST_BODY_LIMIT_BYTES: number;
27
45
  /** True for the formats that cannot stream and so carry the buffered ceiling. */
28
46
  export declare function isBufferedOnlyImportFormat(format: string): boolean;
47
+ /**
48
+ * The refusal every surface gives a JSON or XLSX file over the buffered
49
+ * ceiling. It names the ceiling and the two formats that take a larger file,
50
+ * so the reader knows the fix is a format change, not a plan change.
51
+ */
52
+ export declare function bufferedImportTooLargeMessage(format: string, fileBytes?: number | null): string;