Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/server/provider-openapi.worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,82 @@ describe('openModelNames', () => {
expect(schema?.properties?.model).toBeDefined()
expect(schema?.properties?.model?.enum).toBeUndefined()
})

it('serves TypeSafe POST /v1/systemone with model left open', async () => {
await db
.insert(providers)
.values({
id: 'typesafe',
displayName: 'TypeSafe',
specSourceUrl: 'https://api.typesafe.ai/openapi.json',
})
.onConflictDoNothing()
await db
.insert(endpoints)
.values({
id: 'typesafe/v1/systemone',
providerId: 'typesafe',
activity: 'decision',
method: 'POST',
path: '/v1/systemone',
})
.onConflictDoNothing()
await db
.insert(schemaVersions)
.values([
{
id: 'typesafe/v1/systemone:input',
endpointId: 'typesafe/v1/systemone',
kind: 'input',
contentHash: 'd'.repeat(64),
schema: JSON.stringify(INPUT),
createdAt: NOW,
},
{
id: 'typesafe/v1/systemone:output',
endpointId: 'typesafe/v1/systemone',
kind: 'output',
contentHash: 'e'.repeat(64),
schema: JSON.stringify(OUTPUT),
createdAt: NOW,
},
])
.onConflictDoNothing()
await db
.insert(models)
.values({
id: 'typesafe-jev-1-14-0',
providerId: 'typesafe',
rawId: 'jev-1.14.0',
activity: 'decision',
firstSeenAt: NOW,
lastSeenAt: NOW,
})
.onConflictDoNothing()
const result = await assembleProviderOpenApi(db, 'typesafe')
expect(result.ok).toBe(true)
if (!result.ok) return
const paths = result.document.paths as Record<
string,
{
post?: {
requestBody?: {
content: {
'application/json': {
schema: { properties?: { model?: { enum?: unknown } } }
}
}
}
}
}
>
const post = paths['/v1/systemone']?.post
expect(post).toBeDefined()
expect(
post?.requestBody?.content['application/json'].schema?.properties?.model
?.enum,
).toBeUndefined()
})
})

describe('modelHeader', () => {
Expand Down
11 changes: 11 additions & 0 deletions src/server/providers/adapters/hints.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,5 +264,16 @@
],
"activities": ["world"],
"notes": "Mintlify OpenAPI 3.1. api.worldlabs.ai/openapi.json 404s. Catalog and spec are public. Auth is the WLT-Api-Key header. classifyGets so operations, world get, media-asset get, and credits are kept. Marble 1.1 Plus stays unpriced while its totals are ranges."
},
"typesafe": {
"displayName": "TypeSafe",
"secret": "TYPESAFE_API_KEY",
"strategy": "upstream-spec",
"specUrl": "https://api.typesafe.ai/openapi.json",
"modelsUrl": "https://docs.typesafe.ai/models.md",
"serverUrl": "https://api.typesafe.ai",
"include": ["/v1/systemone"],
"activities": ["decision"],
"notes": "Public OpenAPI 3.1. POST /v1/systemone is the decision call; GET /v1/models is the account list and is dropped. Multipart is stripped so the stored body stays the JSON SystemOneRequest. Catalog and prices are the public models page (output tokens free). model stays an open string."
}
}
151 changes: 151 additions & 0 deletions src/server/providers/adapters/typesafe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Validator } from '@cfworker/json-schema'
import { afterEach, describe, expect, it, vi } from 'vitest'

import { classifyAndBundle } from '../../ingest/sync.ts'
import type { OpenApiDocument } from '../types.ts'
import { jsonSystemOne, provider } from './typesafe.ts'

afterEach(() => {
vi.unstubAllGlobals()
})

const SPEC: OpenApiDocument = {
openapi: '3.1.0',
paths: {
'/v1/models': {
get: {
responses: {
'200': {
description: 'Models',
content: {
'application/json': {
schema: { type: 'object' },
},
},
},
},
},
},
'/v1/systemone': {
post: {
summary: 'System One',
requestBody: {
required: true,
content: {
'application/json': {
schema: { $ref: '#/components/schemas/SystemOneRequest' },
},
'multipart/form-data': {
schema: {
type: 'object',
required: ['request'],
properties: {
request: {
contentMediaType: 'application/json',
contentSchema: {
$ref: '#/components/schemas/SystemOneRequest',
},
},
},
additionalProperties: {
type: 'string',
contentMediaType: 'image/*',
},
},
},
},
},
responses: {
'200': {
description: 'Answers',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/SystemOneResponse' },
},
},
},
},
},
},
},
components: {
schemas: {
SystemOneRequest: {
type: 'object',
required: ['model', 'state', 'questions'],
properties: {
model: { type: 'string' },
state: { type: 'string' },
questions: {
type: 'object',
minProperties: 1,
additionalProperties: {
type: 'object',
properties: { type: { type: 'string', const: 'noul' } },
required: ['type'],
},
},
},
},
SystemOneResponse: {
type: 'object',
required: ['model', 'answers', 'usage'],
properties: {
model: { type: 'string' },
answers: { type: 'object', minProperties: 1 },
usage: {
type: 'object',
required: ['input_tokens', 'output_tokens'],
properties: {
input_tokens: { type: 'integer' },
output_tokens: { type: 'integer' },
},
},
},
},
},
},
}

describe('typesafe', () => {
it('keeps POST /v1/systemone and drops the model list', () => {
expect(provider.classify('/v1/systemone', {})).toBe('decision')
expect(provider.classify('/v1/models', {})).toBeNull()
expect(provider.openModelNames).toBe(true)
expect(provider.authEnvVar).toBe('TYPESAFE_API_KEY')
})

it('bundles the JSON body, not the multipart form', () => {
const bundled = classifyAndBundle(provider, {
specs: [jsonSystemOne(SPEC)],
sources: [{ url: 'https://api.typesafe.ai/openapi.json', hash: 'ab' }],
outputStrategy: 'post-200',
})
expect(bundled.endpoints).toHaveLength(1)
const endpoint = bundled.endpoints[0]
expect(endpoint?.path).toBe('/v1/systemone')
expect(endpoint?.activity).toBe('decision')
expect(endpoint?.input).not.toHaveProperty('properties.request')

const input = new Validator(endpoint?.input as object, '2020-12', false)
expect(
input.validate({
model: 'jev-latest',
state: 'Charged twice.',
questions: { billing: { type: 'noul' } },
}).valid,
).toBe(true)
expect(
input.validate({ model: 'jev-latest', state: 'x', questions: {} }).valid,
).toBe(false)

const output = new Validator(endpoint?.output as object, '2020-12', false)
expect(
output.validate({
model: 'jev-1.14.0',
answers: { billing: { type: 'noul', noul: 0.98 } },
usage: { input_tokens: 120, output_tokens: 12 },
}).valid,
).toBe(true)
})
})
92 changes: 92 additions & 0 deletions src/server/providers/adapters/typesafe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* TypeSafe — Jev, the System One decision model. The public OpenAPI
* document is https://api.typesafe.ai/openapi.json. `POST /v1/systemone`
* also accepts multipart (a JSON part plus image parts). The extractor
* would keep that form and drop the JSON body, which is the request
* TanStack AI sends, so the multipart alternative is removed before
* classify. Images still go in the JSON body as `assets`.
*
* The catalog is the public models page, not `GET /v1/models` (that list
* needs a key and currently names aliases only). Versioned ids are
* accepted whether or not the account list shows them, so `model` stays
* an open string.
*/
import type { Activity } from '#/db/schema.ts'
import { bearerConnect } from '../connect.ts'
import { fetchOpenApi, fetchText } from '../types.ts'
import type {
OpenApiDocument,
ProviderConfig,
ProviderSecrets,
SpecFetchResult,
} from '../types.ts'
import { TYPESAFE_MODELS_URL, typesafeModelInfos } from '../typesafe-models.ts'

export const TYPESAFE_OPENAPI_URL = 'https://api.typesafe.ai/openapi.json'
export const TYPESAFE_API_BASE = 'https://api.typesafe.ai'

/** Keep the JSON body. Multipart is the same request plus image parts. */
export function jsonSystemOne(spec: OpenApiDocument): OpenApiDocument {
const paths = spec.paths
const path = paths?.['/v1/systemone']
const post = path?.post
const requestBody = post?.requestBody
if (
requestBody === null ||
typeof requestBody !== 'object' ||
Array.isArray(requestBody)
) {
throw new Error('typesafe: POST /v1/systemone has no request body')
}
const content = (requestBody as { content?: Record<string, unknown> }).content
if (!content?.['application/json']) {
throw new Error('typesafe: POST /v1/systemone has no JSON body')
}
if (!content['multipart/form-data']) return spec
const next = { ...content }
delete next['multipart/form-data']
return {
...spec,
paths: {
...paths,
'/v1/systemone': {
...path,
post: {
...post,
requestBody: { ...requestBody, content: next },
},
},
},
}
}

function classify(path: string): Activity | null {
return path === '/v1/systemone' ? 'decision' : null
}

async function fetchSpec(_env: ProviderSecrets): Promise<SpecFetchResult> {
const { spec, hash } = await fetchOpenApi(TYPESAFE_OPENAPI_URL)
return {
specs: [jsonSystemOne(spec)],
sources: [{ url: TYPESAFE_OPENAPI_URL, hash }],
outputStrategy: 'post-200',
}
}

export const provider: ProviderConfig = {
id: 'typesafe',
displayName: 'TypeSafe',
authEnvVar: 'TYPESAFE_API_KEY',
specSourceUrl: TYPESAFE_OPENAPI_URL,
modelsEndpoint: TYPESAFE_MODELS_URL,
defaultDerivation: 'upstream-spec',
connect: bearerConnect(TYPESAFE_API_BASE),
openModelNames: true,
fetchSpec,
listModels: async () => ({
models: await typesafeModelInfos(await fetchText(TYPESAFE_MODELS_URL)),
}),
classify,
generationEndpointId: ({ activity }) =>
activity === 'decision' ? 'v1/systemone' : null,
}
5 changes: 5 additions & 0 deletions src/server/providers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ export interface ProviderSecrets {
* the key is `Authorization: Bearer`. Sync and poll do not read it.
*/
FISH_AUDIO_API_KEY?: string
/**
* TypeSafe Jev. The OpenAPI document and the models page are public.
* Sync and poll do not read this. It is the data-plane bearer key.
*/
TYPESAFE_API_KEY?: string
/**
* PostHog project token (phc_…) for ingest failure events. Optional:
* absent means capture is a no-op. US cloud, project 643425.
Expand Down
Loading
Loading