@kindgi/api 0.1.4-rc.4 → 0.1.4

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 (50) hide show
  1. package/dist/app.d.ts +7 -0
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +12 -1
  4. package/dist/app.js.map +1 -1
  5. package/dist/errors.d.ts.map +1 -1
  6. package/dist/errors.js +6 -0
  7. package/dist/errors.js.map +1 -1
  8. package/dist/handler-binding.d.ts +9 -0
  9. package/dist/handler-binding.d.ts.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/middleware/error-mapper.d.ts.map +1 -1
  14. package/dist/middleware/error-mapper.js +2 -0
  15. package/dist/middleware/error-mapper.js.map +1 -1
  16. package/dist/middleware/request-log.d.ts +31 -0
  17. package/dist/middleware/request-log.d.ts.map +1 -0
  18. package/dist/middleware/request-log.js +80 -0
  19. package/dist/middleware/request-log.js.map +1 -0
  20. package/dist/openapi/operations.d.ts.map +1 -1
  21. package/dist/openapi/operations.js +2 -0
  22. package/dist/openapi/operations.js.map +1 -1
  23. package/dist/openapi/schemas.d.ts +2 -0
  24. package/dist/openapi/schemas.d.ts.map +1 -1
  25. package/dist/openapi/schemas.js +35 -0
  26. package/dist/openapi/schemas.js.map +1 -1
  27. package/dist/routes/providers.d.ts.map +1 -1
  28. package/dist/routes/providers.js +78 -5
  29. package/dist/routes/providers.js.map +1 -1
  30. package/dist/routes/runs.d.ts.map +1 -1
  31. package/dist/routes/runs.js +5 -2
  32. package/dist/routes/runs.js.map +1 -1
  33. package/dist/secrets-binding.d.ts +8 -0
  34. package/dist/secrets-binding.d.ts.map +1 -1
  35. package/dist/types.d.ts +9 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/openapi.json +69 -0
  38. package/package.json +22 -21
  39. package/src/app.ts +17 -1
  40. package/src/errors.ts +6 -0
  41. package/src/handler-binding.ts +10 -0
  42. package/src/index.ts +1 -0
  43. package/src/middleware/error-mapper.ts +3 -0
  44. package/src/middleware/request-log.ts +89 -0
  45. package/src/openapi/operations.ts +6 -0
  46. package/src/openapi/schemas.ts +41 -0
  47. package/src/routes/providers.ts +81 -5
  48. package/src/routes/runs.ts +11 -1
  49. package/src/secrets-binding.ts +5 -0
  50. package/src/types.ts +9 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kindgi/api",
3
- "version": "0.1.4-rc.4",
3
+ "version": "0.1.4",
4
4
  "description": "REST + SSE HTTP surface for Kindgi™. createApp assembles a Hono app serving the /v1/* REST API (bearer or session-token auth; OpenAPI 3.1 document at /v1/openapi.json) and an optional S3-compatible /s3/* surface (SigV4), over caller-plugged bindings for the runtime (runs, agents, flows, HITL, supervisor) and for storage. Route conventions: docs/API-ROUTE-CONVENTIONS.md.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -32,30 +32,31 @@
32
32
  "openapi.json"
33
33
  ],
34
34
  "dependencies": {
35
- "@kindgi/agents": "0.1.4-rc.4",
36
- "@kindgi/audit-events": "0.1.4-rc.4",
37
- "@kindgi/compliance": "0.1.4-rc.4",
38
- "@kindgi/authz": "0.1.4-rc.4",
39
- "@kindgi/blob-binding": "0.1.4-rc.4",
40
- "@kindgi/capabilities": "0.1.4-rc.4",
41
- "@kindgi/crypto": "0.1.4-rc.4",
42
- "@kindgi/flow": "0.1.4-rc.4",
43
- "@kindgi/guardrails": "0.1.4-rc.4",
44
- "@kindgi/memory": "0.1.4-rc.4",
45
- "@kindgi/platform": "0.1.4-rc.4",
46
- "@kindgi/provenance": "0.1.4-rc.4",
47
- "@kindgi/runtime": "0.1.4-rc.4",
48
- "@kindgi/policy-contract": "0.1.4-rc.4",
49
- "@kindgi/schema": "0.1.4-rc.4",
50
- "@kindgi/tools": "0.1.4-rc.4",
51
- "@kindgi/types": "0.1.4-rc.4",
35
+ "@kindgi/agents": "0.1.4",
36
+ "@kindgi/audit-events": "0.1.4",
37
+ "@kindgi/compliance": "0.1.4",
38
+ "@kindgi/authz": "0.1.4",
39
+ "@kindgi/blob-binding": "0.1.4",
40
+ "@kindgi/capabilities": "0.1.4",
41
+ "@kindgi/crypto": "0.1.4",
42
+ "@kindgi/flow": "0.1.4",
43
+ "@kindgi/guardrails": "0.1.4",
44
+ "@kindgi/log": "0.1.4",
45
+ "@kindgi/memory": "0.1.4",
46
+ "@kindgi/platform": "0.1.4",
47
+ "@kindgi/provenance": "0.1.4",
48
+ "@kindgi/runtime": "0.1.4",
49
+ "@kindgi/policy-contract": "0.1.4",
50
+ "@kindgi/schema": "0.1.4",
51
+ "@kindgi/tools": "0.1.4",
52
+ "@kindgi/types": "0.1.4",
52
53
  "@scalar/hono-api-reference": "^0.12.2",
53
54
  "hono": "^4.6.14"
54
55
  },
55
56
  "devDependencies": {
56
- "@kindgi/audit-events-inmemory": "0.1.4-rc.4",
57
- "@kindgi/specs": "0.1.4-rc.4",
58
- "@kindgi/testing": "0.1.4-rc.4",
57
+ "@kindgi/audit-events-inmemory": "0.1.4",
58
+ "@kindgi/specs": "0.1.4",
59
+ "@kindgi/testing": "0.1.4",
59
60
  "@types/aws4": "^1.11.6",
60
61
  "@types/node": "^22.10.5",
61
62
  "aws4": "^1.13.2",
package/src/app.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
+ import { type Logger, noopLogger } from '@kindgi/log';
4
5
  import { Scalar } from '@scalar/hono-api-reference';
5
6
  import { Hono } from 'hono';
6
7
 
@@ -63,6 +64,7 @@ import { principalMiddleware } from './middleware/principal.js';
63
64
  import { PROJECT_REF_ROUTES, refuseBadProjectId } from './middleware/project-ref.js';
64
65
  import { publicRunCorsMiddleware, publicRunRouteMatcher } from './middleware/public-run-routes.js';
65
66
  import { requestIdMiddleware } from './middleware/request-id.js';
67
+ import { requestLogMiddleware } from './middleware/request-log.js';
66
68
  import { sigv4Middleware } from './middleware/sigv4.js';
67
69
  import { type GenerateOptions, generateOpenApiDocument } from './openapi/generate.js';
68
70
  import type { ProvenanceBinding } from './provenance-binding.js';
@@ -150,6 +152,12 @@ import type { WebhookEndpointBinding } from './webhook-endpoint-binding.js';
150
152
  * for tests.
151
153
  */
152
154
  export interface CreateAppInput {
155
+ /**
156
+ * Where the app's records go (`@kindgi/log`): the access line, logged
157
+ * 500s, and what routes log, each with the request's ids. Default:
158
+ * `noopLogger`, so an embedding app stays quiet unless it passes one.
159
+ */
160
+ readonly logger?: Logger;
153
161
  readonly resolveToken: TokenResolver;
154
162
  readonly runHandler: RunHandlerBinding;
155
163
  /**
@@ -893,7 +901,9 @@ export function createApp(input: CreateAppInput): Hono<AppEnv> {
893
901
 
894
902
  // ---------- global middleware ----------
895
903
  app.use('*', requestIdMiddleware());
896
- // A thrown exception: a 500 wire error with its message and request id.
904
+ // The request's trace context and logger, and its access line.
905
+ app.use('*', requestLogMiddleware(input.logger ?? noopLogger));
906
+ // A thrown exception: a 500 wire error with its message and request id, logged.
897
907
  app.onError(mapThrownError);
898
908
 
899
909
  // Public run tokens: checked once at startup; CORS for the two routes
@@ -968,6 +978,12 @@ export function createApp(input: CreateAppInput): Hono<AppEnv> {
968
978
  // still populate the principal (cheap, and lets `can`/`check` work
969
979
  // as inspection helpers even when authorize() enforcement is off).
970
980
  v1.use('*', principalMiddleware());
981
+ // From here on, the request's records carry its tenant.
982
+ v1.use('*', async (c, next) => {
983
+ const tenantId = c.get('tenantId');
984
+ if (tenantId !== undefined) c.set('log', c.get('log').child({ tenantId }));
985
+ await next();
986
+ });
971
987
  const authorizer: Authorizer | undefined =
972
988
  input.authz !== undefined ? createAuthorizer(input.authz.authzCheckBinding) : undefined;
973
989
 
package/src/errors.ts CHANGED
@@ -88,6 +88,10 @@ export const ERROR_CODE_TO_STATUS: Readonly<Record<string, number>> = {
88
88
  'budget-exceeded': 422,
89
89
  'output-schema-violation': 422,
90
90
  'model-invocation-failed': 422,
91
+ /** No registered provider satisfies the agent's capability declaration (`needs`). */
92
+ 'capability-unsatisfiable': 422,
93
+ /** A tool the agent names has no version satisfying its range (or the pinned one is gone). */
94
+ 'tool-version-unresolvable': 422,
91
95
  'tool-invocation-failed': 422,
92
96
  'capability-routing-failed': 422,
93
97
  'runtime-not-configured': 422,
@@ -287,6 +291,8 @@ export const ERROR_CODE_TO_STATUS: Readonly<Record<string, number>> = {
287
291
  'secret-provider-unauthorized': 502,
288
292
  'secret-provider-rate-limited': 429,
289
293
  'secret-store-error': 500,
294
+ /** The secret store doesn't do this by design (the dev store's rotate and revoke). */
295
+ 'secret-operation-unsupported': 501,
290
296
  'env-store-error': 500,
291
297
  // Trigger HTTP surface.
292
298
  'trigger-not-found': 404,
@@ -58,6 +58,12 @@ export interface RunHandlerBinding {
58
58
  resumeRun(input: ResumeRunBindingInput): Promise<RunHandlerOutcome>;
59
59
  }
60
60
 
61
+ /** The request's W3C trace context, for the run it starts (`traceId` is kept on the run). */
62
+ export interface RunTrace {
63
+ readonly traceId: string;
64
+ readonly spanId: string;
65
+ }
66
+
61
67
  export interface ResumeRunBindingInput {
62
68
  readonly tenantId: TenantId;
63
69
  readonly runId: RunId;
@@ -101,6 +107,8 @@ export interface InvokeAgentBindingInput {
101
107
  * can't run in the background may treat `false` like `true`.
102
108
  */
103
109
  readonly wait?: boolean;
110
+ /** The trace context of the request starting the run: the binding records its `traceId` on the run. */
111
+ readonly trace?: RunTrace;
104
112
  }
105
113
 
106
114
  export interface InvokeFlowBindingInput {
@@ -122,6 +130,8 @@ export interface InvokeFlowBindingInput {
122
130
  readonly wait?: boolean;
123
131
  /** Agents and tools to run at other exact versions than the flow version's pins (`RunFlowInput.versions`). */
124
132
  readonly versions?: FlowVersionOverrides;
133
+ /** The trace context of the request starting the run, as for an agent run. */
134
+ readonly trace?: RunTrace;
125
135
  }
126
136
 
127
137
  export type RunHandlerOutcome =
package/src/index.ts CHANGED
@@ -98,6 +98,7 @@ export type {
98
98
  RunHandlerBinding,
99
99
  RunHandlerFailure,
100
100
  RunHandlerOutcome,
101
+ RunTrace,
101
102
  } from './handler-binding.js';
102
103
  export type {
103
104
  ReviewerBinding,
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
+ import type { Logger } from '@kindgi/log';
4
5
  import type { Context } from 'hono';
5
6
 
6
7
  import { statusFor, toWireError } from '../errors.js';
@@ -28,6 +29,8 @@ export function mapThrownError(cause: Error, c: Context): Response {
28
29
  return c.newResponse(res.body, res);
29
30
  }
30
31
  const requestId = (c.get('requestId') as string | undefined) ?? 'req-unknown';
32
+ // A 500 is something an operator should look at: logged, with the error.
33
+ (c.get('log') as Logger | undefined)?.error('unhandled error', { err: cause });
31
34
  const body = toWireError(
32
35
  {
33
36
  code: 'internal-server-error',
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import { type LogLevel, type Logger, formatTraceparent, traceFromHeader } from '@kindgi/log';
5
+ import type { Context, MiddlewareHandler } from 'hono';
6
+ import { routePath } from 'hono/route';
7
+
8
+ import type { AppEnv } from '../types.js';
9
+
10
+ /** Paths a client or a probe reads all the time: their lines are `debug` even when they fail to read. */
11
+ const QUIET_PATHS: ReadonlySet<string> = new Set(['/health', '/ready', '/v1/openapi.json']);
12
+
13
+ const WRITES: ReadonlySet<string> = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
14
+
15
+ /**
16
+ * The access line's level, so `info` stays readable with a console open
17
+ * (it polls every few seconds): a write, and every 4xx, at `info`; every
18
+ * 5xx at `error`; a successful read (`GET`, `HEAD`: the polling), the
19
+ * health and readiness probes, the spec and the console's files, and a
20
+ * stream's opening, at `debug`.
21
+ */
22
+ export function accessLevel(c: Context<AppEnv>): LogLevel {
23
+ const status = c.res.status;
24
+ if (status >= 500) return 'error';
25
+ if (QUIET_PATHS.has(c.req.path) || !c.req.path.startsWith('/v1/')) return 'debug';
26
+ if (WRITES.has(c.req.method) || status >= 400) return 'info';
27
+ return 'debug';
28
+ }
29
+
30
+ /**
31
+ * The request's trace context and logger, then its access line.
32
+ *
33
+ * - **Trace:** an incoming `traceparent` (a load balancer's, an API
34
+ * gateway's, the caller's own app) is honoured: its trace id is kept
35
+ * and the request gets its own span, a child of the caller's. A
36
+ * missing or malformed one starts a fresh trace. `c.var.trace` holds
37
+ * it; the response answers `traceresponse` (W3C Trace Context Level
38
+ * 2) next to `X-Request-Id`.
39
+ * - **Logger:** `c.var.log` is a child of the app's logger with the
40
+ * request's `requestId`, `traceId` and `spanId` (subsystem `http`);
41
+ * routes log through it, and authentication adds `tenantId`.
42
+ * - **Access line:** `METHOD route status ms`, with the route's
43
+ * pattern (`/v1/runs/:runId`, never the raw path, so ids don't
44
+ * multiply lines), at the level `accessLevel` gives.
45
+ *
46
+ * Mounted right after the request-id middleware.
47
+ */
48
+ export function requestLogMiddleware(logger: Logger): MiddlewareHandler<AppEnv> {
49
+ return async (c, next) => {
50
+ const started = Date.now();
51
+ const trace = traceFromHeader(c.req.header('traceparent'));
52
+ c.set('trace', trace);
53
+ c.set(
54
+ 'log',
55
+ logger.child({
56
+ subsystem: 'http',
57
+ requestId: c.get('requestId'),
58
+ traceId: trace.traceId,
59
+ spanId: trace.spanId,
60
+ }),
61
+ );
62
+ try {
63
+ await next();
64
+ } finally {
65
+ c.header('traceresponse', formatTraceparent(trace));
66
+ const log = c.get('log');
67
+ const level = accessLevel(c);
68
+ if (log.isLevelEnabled(level)) {
69
+ // The responding route's pattern; a request no route matched ends on a wildcard.
70
+ const pattern = routePath(c, -1);
71
+ const route = pattern.endsWith('*') ? c.req.path : pattern;
72
+ const durationMs = Date.now() - started;
73
+ const stream = (c.res.headers.get('content-type') ?? '').startsWith('text/event-stream');
74
+ log[stream ? 'debug' : level](
75
+ `${c.req.method} ${route} ${c.res.status} ${durationMs}ms${stream ? ' (stream opened)' : ''}`,
76
+ {
77
+ method: c.req.method,
78
+ route,
79
+ status: c.res.status,
80
+ durationMs,
81
+ ...(c.get('tenantId') !== undefined && { tenantId: c.get('tenantId') }),
82
+ },
83
+ // The message says them: a terminal line needn't again.
84
+ { inMessage: ['method', 'route', 'status', 'durationMs'] },
85
+ );
86
+ }
87
+ }
88
+ };
89
+ }
@@ -6082,6 +6082,9 @@ export const OPERATIONS: readonly OperationSpec[] = [
6082
6082
  ...CommonMutationErrors,
6083
6083
  '403': ErrorResponse('Bearer token missing `secrets:rotate` capability.'),
6084
6084
  '404': ErrorResponse('Unknown secret.'),
6085
+ '501': ErrorResponse(
6086
+ "The store doesn't rotate (`secret-operation-unsupported`): under `kindgi dev` secrets live in the env files and have no versions; the message says what to do instead.",
6087
+ ),
6085
6088
  },
6086
6089
  },
6087
6090
  {
@@ -6178,6 +6181,9 @@ export const OPERATIONS: readonly OperationSpec[] = [
6178
6181
  '400': ErrorResponse('Missing or malformed scope / envName.'),
6179
6182
  '403': ErrorResponse('Bearer token missing revoke capability.'),
6180
6183
  '404': ErrorResponse('Unknown secret.'),
6184
+ '501': ErrorResponse(
6185
+ "The store doesn't revoke (`secret-operation-unsupported`): under `kindgi dev` secrets live in the env files; the message says to remove the name there.",
6186
+ ),
6181
6187
  },
6182
6188
  },
6183
6189
 
@@ -637,6 +637,12 @@ export const RunSchema: JsonSchema = {
637
637
  description:
638
638
  "The segment path the run was started with (coarse to fine), which picks live agent versions. A child run has its parent's. Absent when there was none.",
639
639
  },
640
+ traceId: {
641
+ type: 'string',
642
+ pattern: '^[0-9a-f]{32}$',
643
+ description:
644
+ "The W3C trace id of the request that started the run: the caller's (from its `traceparent`) or one the API minted. The runtime's records about the run carry it; `GET` responses answer `traceresponse` with each request's own. Absent for a run no request started, and on runs from before runs recorded it.",
645
+ },
640
646
  publicAccessToken: {
641
647
  type: 'string',
642
648
  description:
@@ -4010,6 +4016,28 @@ export const CapabilityCollectionPageSchema: JsonSchema = {
4010
4016
  },
4011
4017
  };
4012
4018
 
4019
+ /** `ModelInfo.thinking`: how a model thinks, so a judge can ask for its least. */
4020
+ export const ModelThinkingSchema: JsonSchema = {
4021
+ type: 'object',
4022
+ additionalProperties: false,
4023
+ required: ['mode', 'lowest'],
4024
+ description:
4025
+ "How the model thinks before it answers, so a call that wants as little as it allows (a judge's) gets it. Absent: it doesn't think, or nothing is known.",
4026
+ properties: {
4027
+ mode: {
4028
+ type: 'string',
4029
+ enum: ['adaptive', 'always'],
4030
+ description: '`adaptive`: on unless turned down. `always`: on, and it can only be lowered.',
4031
+ },
4032
+ lowest: {
4033
+ type: 'string',
4034
+ minLength: 1,
4035
+ description:
4036
+ "The vendor's own setting for the least thinking: for Anthropic `disabled`, `between_tools` or an effort (`low`); for Gemini a thinking level (`low`, `minimal`); for OpenAI a reasoning effort (`low`, `none`).",
4037
+ },
4038
+ },
4039
+ };
4040
+
4013
4041
  export const ProviderCostSchema: JsonSchema = {
4014
4042
  type: 'object',
4015
4043
  // Adapters widen the cost table with their own rates (Anthropic's
@@ -4061,6 +4089,12 @@ export const ModelInfoSchema: JsonSchema = {
4061
4089
  description:
4062
4090
  'Fallback cap on output tokens. Adapters that require `max_tokens` on every request (e.g. Anthropic) use this when `ModelCallInput.maxOutputTokens` is unset.',
4063
4091
  },
4092
+ sampling: {
4093
+ type: 'boolean',
4094
+ description:
4095
+ "Whether the model takes sampling settings (`temperature`). `false`: its API rejects a non-default value, so the call goes without one and the answer's `warnings` say so (`sampling-unsupported`). Absent: it takes them.",
4096
+ },
4097
+ thinking: { $ref: '#/components/schemas/ModelThinking' },
4064
4098
  description: {
4065
4099
  type: 'string',
4066
4100
  description: 'Short per-model description surfaced in logs.',
@@ -4098,6 +4132,12 @@ export const ProviderMetadataSchema: JsonSchema = {
4098
4132
  description:
4099
4133
  'Models this connection exposes. Non-empty. `models[i].name` must be unique within the list.',
4100
4134
  },
4135
+ defaultModel: {
4136
+ type: 'string',
4137
+ minLength: 1,
4138
+ description:
4139
+ "The model to use when an agent doesn't choose: one of `models[].name`. When candidates rank equally, it comes before the provider's other models; without it, ties break by model name. A preset sets it. A runtime before 0.1.4 ignores it.",
4140
+ },
4101
4141
  attributes: {
4102
4142
  type: 'array',
4103
4143
  items: { type: 'string' },
@@ -8414,6 +8454,7 @@ export const COMPONENT_SCHEMAS: ReadonlyArray<readonly [string, JsonSchema]> = [
8414
8454
  ['Feature', FeatureSchema],
8415
8455
  ['CapabilityDescriptor', CapabilityDescriptorSchema],
8416
8456
  ['CapabilityCollectionPage', CapabilityCollectionPageSchema],
8457
+ ['ModelThinking', ModelThinkingSchema],
8417
8458
  ['ProviderCost', ProviderCostSchema],
8418
8459
  ['ModelInfo', ModelInfoSchema],
8419
8460
  ['ProviderMetadata', ProviderMetadataSchema],
@@ -9,6 +9,7 @@ import {
9
9
  type Feature,
10
10
  type ModelInfo,
11
11
  type ProviderMetadata,
12
+ isModelThinking,
12
13
  validateProviderLabels,
13
14
  } from '@kindgi/capabilities';
14
15
  import type { Cursor, TenantId } from '@kindgi/types';
@@ -305,14 +306,15 @@ function serializeProvider(m: ProviderMetadata): Record<string, unknown> {
305
306
  name: model.name,
306
307
  contextWindow: model.contextWindow,
307
308
  features: model.features,
308
- cost: {
309
- promptUsdPer1kTokens: model.cost.promptUsdPer1kTokens,
310
- completionUsdPer1kTokens: model.cost.completionUsdPer1kTokens,
311
- },
309
+ // As stored: the base rates and any an adapter widens it with.
310
+ cost: { ...model.cost },
312
311
  ...(model.p95LatencyMs !== undefined && { p95LatencyMs: model.p95LatencyMs }),
313
312
  ...(model.maxOutputTokens !== undefined && { maxOutputTokens: model.maxOutputTokens }),
313
+ ...(model.sampling !== undefined && { sampling: model.sampling }),
314
+ ...(model.thinking !== undefined && { thinking: model.thinking }),
314
315
  ...(model.description !== undefined && { description: model.description }),
315
316
  })),
317
+ ...(m.defaultModel !== undefined && { defaultModel: m.defaultModel }),
316
318
  ...(m.attributes !== undefined && { attributes: m.attributes }),
317
319
  ...(m.description !== undefined && { description: m.description }),
318
320
  ...(m.capabilityKind !== undefined && { capabilityKind: m.capabilityKind }),
@@ -436,10 +438,20 @@ function validateProviderMetadata(
436
438
  }
437
439
  const badLabels = validateProviderLabels(b.id, b.labels);
438
440
  if (badLabels !== undefined) return { kind: 'err', error: badLabels };
441
+ if (b.defaultModel !== undefined && !seenNames.has(b.defaultModel as string)) {
442
+ return {
443
+ kind: 'err',
444
+ error: {
445
+ message: `provider "${b.id}" defaultModel must be one of its models (${[...seenNames].join(', ')}), got ${JSON.stringify(b.defaultModel)}`,
446
+ reason: 'unknown-default-model',
447
+ },
448
+ };
449
+ }
439
450
  const value: ProviderMetadata = {
440
451
  id: b.id,
441
452
  region: b.region,
442
453
  models: validatedModels,
454
+ ...(b.defaultModel !== undefined && { defaultModel: b.defaultModel as string }),
443
455
  ...(b.attributes !== undefined && { attributes: b.attributes as readonly string[] }),
444
456
  ...(b.description !== undefined && { description: b.description }),
445
457
  ...(b.capabilityKind !== undefined && { capabilityKind: b.capabilityKind as string }),
@@ -527,6 +539,16 @@ function validateModelInfo(
527
539
  },
528
540
  };
529
541
  }
542
+ const extraRates = costExtraRates(cost as unknown as Record<string, unknown>);
543
+ if (extraRates === undefined) {
544
+ return {
545
+ kind: 'err',
546
+ error: {
547
+ message: `provider "${providerId}" model "${m.name}" cost table's other rates must be non-negative numbers, or objects of them (e.g. longContext: { thresholdTokens, promptUsdPer1kTokens, completionUsdPer1kTokens })`,
548
+ reason: 'invalid-cost',
549
+ },
550
+ };
551
+ }
530
552
  if (m.p95LatencyMs !== undefined) {
531
553
  if (typeof m.p95LatencyMs !== 'number' || m.p95LatencyMs < 0) {
532
554
  return {
@@ -553,6 +575,24 @@ function validateModelInfo(
553
575
  };
554
576
  }
555
577
  }
578
+ if (m.sampling !== undefined && typeof m.sampling !== 'boolean') {
579
+ return {
580
+ kind: 'err',
581
+ error: {
582
+ message: `provider "${providerId}" model "${m.name}" sampling must be true or false`,
583
+ reason: 'invalid-sampling',
584
+ },
585
+ };
586
+ }
587
+ if (m.thinking !== undefined && !isModelThinking(m.thinking)) {
588
+ return {
589
+ kind: 'err',
590
+ error: {
591
+ message: `provider "${providerId}" model "${m.name}" thinking must be { mode: 'adaptive' | 'always', lowest: <the vendor's setting> }`,
592
+ reason: 'invalid-thinking',
593
+ },
594
+ };
595
+ }
556
596
  if (m.description !== undefined && typeof m.description !== 'string') {
557
597
  return {
558
598
  kind: 'err',
@@ -567,16 +607,52 @@ function validateModelInfo(
567
607
  contextWindow: m.contextWindow,
568
608
  features: m.features as readonly Feature[],
569
609
  cost: {
610
+ ...extraRates,
570
611
  promptUsdPer1kTokens: cost.promptUsdPer1kTokens,
571
612
  completionUsdPer1kTokens: cost.completionUsdPer1kTokens,
572
- },
613
+ } as ModelInfo['cost'],
573
614
  ...(m.p95LatencyMs !== undefined && { p95LatencyMs: m.p95LatencyMs }),
574
615
  ...(m.maxOutputTokens !== undefined && { maxOutputTokens: m.maxOutputTokens }),
616
+ ...(m.sampling !== undefined && { sampling: m.sampling }),
617
+ ...(m.thinking !== undefined && {
618
+ thinking: { mode: m.thinking.mode, lowest: m.thinking.lowest },
619
+ }),
575
620
  ...(m.description !== undefined && { description: m.description }),
576
621
  };
577
622
  return { kind: 'ok', value };
578
623
  }
579
624
 
625
+ /**
626
+ * The rates a cost table carries beyond the two base ones: an adapter
627
+ * widens it with its own (Anthropic's prompt-cache multipliers, Gemini's
628
+ * cached-prompt share, a long-context tier), each a non-negative number
629
+ * or one level of an object of them. Returned as given, or `undefined`
630
+ * when one isn't, so a registration keeps what its adapter prices with.
631
+ */
632
+ function costExtraRates(
633
+ cost: Readonly<Record<string, unknown>>,
634
+ ): Record<string, number | Record<string, number>> | undefined {
635
+ const isRate = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v >= 0;
636
+ const out: Record<string, number | Record<string, number>> = {};
637
+ for (const [key, value] of Object.entries(cost)) {
638
+ if (key === 'promptUsdPer1kTokens' || key === 'completionUsdPer1kTokens') continue;
639
+ if (isRate(value)) {
640
+ out[key] = value;
641
+ } else if (
642
+ typeof value === 'object' &&
643
+ value !== null &&
644
+ !Array.isArray(value) &&
645
+ Object.keys(value).length > 0 &&
646
+ Object.values(value).every(isRate)
647
+ ) {
648
+ out[key] = { ...(value as Record<string, number>) };
649
+ } else {
650
+ return undefined;
651
+ }
652
+ }
653
+ return out;
654
+ }
655
+
580
656
  /**
581
657
  * Parse the optional `adapter_config` on the register request body: a
582
658
  * flat object of string, number or boolean values. Credentials don't
@@ -23,6 +23,7 @@ import type {
23
23
  RunHandlerBinding,
24
24
  RunHandlerFailure,
25
25
  RunHandlerOutcome,
26
+ RunTrace,
26
27
  } from '../handler-binding.js';
27
28
  import type { Authorizer } from '../middleware/authorize.js';
28
29
  import type { MintPublicRunTokenResult } from '../public-run-token.js';
@@ -161,7 +162,13 @@ export function runsRouter(
161
162
  return c.json(toWireError(parsed.error, requestId));
162
163
  }
163
164
 
164
- const invocation = await invokeFromBody(binding, tenantId, parsed.value);
165
+ const trace = c.get('trace');
166
+ const invocation = await invokeFromBody(
167
+ binding,
168
+ tenantId,
169
+ parsed.value,
170
+ trace !== undefined ? { traceId: trace.traceId, spanId: trace.spanId } : undefined,
171
+ );
165
172
 
166
173
  if (invocation.kind === 'err') {
167
174
  c.status(statusFor(invocation.error.code) as never);
@@ -624,9 +631,11 @@ function invokeFromBody(
624
631
  binding: RunHandlerBinding,
625
632
  tenantId: TenantId,
626
633
  body: ParsedStartRunBody,
634
+ trace?: RunTrace,
627
635
  ): Promise<RunHandlerOutcome> {
628
636
  const common = {
629
637
  tenantId,
638
+ ...(trace !== undefined && { trace }),
630
639
  ...(body.projectId !== undefined && { projectId: body.projectId }),
631
640
  input: body.input,
632
641
  ...(body.segments !== undefined && { segments: body.segments }),
@@ -690,6 +699,7 @@ function serializeRun(
690
699
  row.segments.length > 0 && {
691
700
  segments: row.segments.map(({ key, value }) => ({ key, value })),
692
701
  }),
702
+ ...(row.traceId != null && { traceId: row.traceId }),
693
703
  };
694
704
  }
695
705
 
@@ -305,4 +305,9 @@ export type SecretError =
305
305
  readonly message: string;
306
306
  readonly currentVersion: number;
307
307
  }
308
+ /**
309
+ * The store doesn't do this by design (the dev store keeps no versions to
310
+ * rotate and no revocation): the message says what to do instead.
311
+ */
312
+ | { readonly code: 'secret-operation-unsupported'; readonly message: string }
308
313
  | { readonly code: 'secret-store-error'; readonly message: string; readonly cause?: unknown };
package/src/types.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
4
  import type { Principal, ReviewerRole } from '@kindgi/authz';
5
+ import type { Logger, TraceContext } from '@kindgi/log';
5
6
  import type { ApiTokenId, RunId, SessionId, TenantId, UserId } from '@kindgi/types';
6
7
 
7
8
  /**
@@ -11,6 +12,14 @@ import type { ApiTokenId, RunId, SessionId, TenantId, UserId } from '@kindgi/typ
11
12
  export interface AppEnv {
12
13
  Variables: {
13
14
  requestId: string;
15
+ /**
16
+ * The request's logger (`requestLogMiddleware`): the app's, with the
17
+ * request's `requestId`, `traceId` and `spanId`, and its `tenantId`
18
+ * once authenticated. Routes log through it.
19
+ */
20
+ log: Logger;
21
+ /** The request's W3C trace context: the caller's trace when it sent a valid `traceparent`. */
22
+ trace: TraceContext;
14
23
  /** Set by `bearerAuthMiddleware` on authenticated routes. */
15
24
  tenantId: TenantId;
16
25
  /**