add experimental provider auth HttpApi slice (#22389)
This commit is contained in:
@@ -358,7 +358,6 @@
|
|||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
|
||||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||||
"@opentelemetry/api": "1.9.0",
|
"@opentelemetry/api": "1.9.0",
|
||||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||||
@@ -506,17 +505,6 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/server": {
|
|
||||||
"name": "@opencode-ai/server",
|
|
||||||
"version": "1.4.6",
|
|
||||||
"dependencies": {
|
|
||||||
"effect": "catalog:",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
"typescript": "catalog:",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/shared": {
|
"packages/shared": {
|
||||||
"name": "@opencode-ai/shared",
|
"name": "@opencode-ai/shared",
|
||||||
"version": "1.4.6",
|
"version": "1.4.6",
|
||||||
@@ -1568,8 +1556,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"],
|
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"],
|
||||||
|
|
||||||
"@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"],
|
|
||||||
|
|
||||||
"@opencode-ai/shared": ["@opencode-ai/shared@workspace:packages/shared"],
|
"@opencode-ai/shared": ["@opencode-ai/shared@workspace:packages/shared"],
|
||||||
|
|
||||||
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
||||||
|
|||||||
@@ -115,7 +115,6 @@
|
|||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
|
||||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||||
"@opentelemetry/api": "1.9.0",
|
"@opentelemetry/api": "1.9.0",
|
||||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||||
|
|||||||
@@ -156,6 +156,14 @@ Ordering for a route-group migration:
|
|||||||
3. move tagged route-facing errors to `Schema.TaggedErrorClass` where needed
|
3. move tagged route-facing errors to `Schema.TaggedErrorClass` where needed
|
||||||
4. switch existing Zod boundary validators to derived `.zod`
|
4. switch existing Zod boundary validators to derived `.zod`
|
||||||
5. define the `HttpApi` contract from the canonical Effect schemas
|
5. define the `HttpApi` contract from the canonical Effect schemas
|
||||||
|
6. regenerate the SDK (`./packages/sdk/js/script/build.ts`) and verify zero diff against `dev`
|
||||||
|
|
||||||
|
SDK shape rule:
|
||||||
|
|
||||||
|
- every schema migration must preserve the generated SDK output byte-for-byte
|
||||||
|
- `Schema.Class` emits a named `$ref` in OpenAPI via its identifier — use it only for types that already had `.meta({ ref })` in the old Zod schema
|
||||||
|
- inner / nested types that were anonymous in the old Zod schema should stay as `Schema.Struct` (not `Schema.Class`) to avoid introducing new named components in the OpenAPI spec
|
||||||
|
- if a diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging
|
||||||
|
|
||||||
Temporary exception:
|
Temporary exception:
|
||||||
|
|
||||||
@@ -195,8 +203,9 @@ Use the same sequence for each route group.
|
|||||||
4. Define the `HttpApi` contract separately from the handlers.
|
4. Define the `HttpApi` contract separately from the handlers.
|
||||||
5. Implement handlers by yielding the existing service from context.
|
5. Implement handlers by yielding the existing service from context.
|
||||||
6. Mount the new surface in parallel under an experimental prefix.
|
6. Mount the new surface in parallel under an experimental prefix.
|
||||||
7. Add one end-to-end test and one OpenAPI-focused test.
|
7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above).
|
||||||
8. Compare ergonomics before migrating the next endpoint.
|
8. Add one end-to-end test and one OpenAPI-focused test.
|
||||||
|
9. Compare ergonomics before migrating the next endpoint.
|
||||||
|
|
||||||
Rule of thumb:
|
Rule of thumb:
|
||||||
|
|
||||||
|
|||||||
@@ -2,70 +2,62 @@ import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
|
|||||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||||
import { Auth } from "@/auth"
|
import { Auth } from "@/auth"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { zod } from "@/util/effect-zod"
|
||||||
|
import { withStatics } from "@/util/schema"
|
||||||
import { Plugin } from "../plugin"
|
import { Plugin } from "../plugin"
|
||||||
import { ProviderID } from "./schema"
|
import { ProviderID } from "./schema"
|
||||||
import { Array as Arr, Effect, Layer, Record, Result, Context } from "effect"
|
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
export namespace ProviderAuth {
|
export namespace ProviderAuth {
|
||||||
export const Method = z
|
const When = Schema.Struct({
|
||||||
.object({
|
key: Schema.String,
|
||||||
type: z.union([z.literal("oauth"), z.literal("api")]),
|
op: Schema.Literals(["eq", "neq"]),
|
||||||
label: z.string(),
|
value: Schema.String,
|
||||||
prompts: z
|
})
|
||||||
.array(
|
|
||||||
z.union([
|
|
||||||
z.object({
|
|
||||||
type: z.literal("text"),
|
|
||||||
key: z.string(),
|
|
||||||
message: z.string(),
|
|
||||||
placeholder: z.string().optional(),
|
|
||||||
when: z
|
|
||||||
.object({
|
|
||||||
key: z.string(),
|
|
||||||
op: z.union([z.literal("eq"), z.literal("neq")]),
|
|
||||||
value: z.string(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
type: z.literal("select"),
|
|
||||||
key: z.string(),
|
|
||||||
message: z.string(),
|
|
||||||
options: z.array(
|
|
||||||
z.object({
|
|
||||||
label: z.string(),
|
|
||||||
value: z.string(),
|
|
||||||
hint: z.string().optional(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
when: z
|
|
||||||
.object({
|
|
||||||
key: z.string(),
|
|
||||||
op: z.union([z.literal("eq"), z.literal("neq")]),
|
|
||||||
value: z.string(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
.meta({
|
|
||||||
ref: "ProviderAuthMethod",
|
|
||||||
})
|
|
||||||
export type Method = z.infer<typeof Method>
|
|
||||||
|
|
||||||
export const Authorization = z
|
const TextPrompt = Schema.Struct({
|
||||||
.object({
|
type: Schema.Literal("text"),
|
||||||
url: z.string(),
|
key: Schema.String,
|
||||||
method: z.union([z.literal("auto"), z.literal("code")]),
|
message: Schema.String,
|
||||||
instructions: z.string(),
|
placeholder: Schema.optional(Schema.String),
|
||||||
})
|
when: Schema.optional(When),
|
||||||
.meta({
|
})
|
||||||
ref: "ProviderAuthAuthorization",
|
|
||||||
})
|
const SelectOption = Schema.Struct({
|
||||||
export type Authorization = z.infer<typeof Authorization>
|
label: Schema.String,
|
||||||
|
value: Schema.String,
|
||||||
|
hint: Schema.optional(Schema.String),
|
||||||
|
})
|
||||||
|
|
||||||
|
const SelectPrompt = Schema.Struct({
|
||||||
|
type: Schema.Literal("select"),
|
||||||
|
key: Schema.String,
|
||||||
|
message: Schema.String,
|
||||||
|
options: Schema.Array(SelectOption),
|
||||||
|
when: Schema.optional(When),
|
||||||
|
})
|
||||||
|
|
||||||
|
const Prompt = Schema.Union([TextPrompt, SelectPrompt])
|
||||||
|
|
||||||
|
export class Method extends Schema.Class<Method>("ProviderAuthMethod")({
|
||||||
|
type: Schema.Literals(["oauth", "api"]),
|
||||||
|
label: Schema.String,
|
||||||
|
prompts: Schema.optional(Schema.Array(Prompt)),
|
||||||
|
}) {
|
||||||
|
static readonly zod = zod(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Methods = Schema.Record(Schema.String, Schema.Array(Method)).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
|
export type Methods = typeof Methods.Type
|
||||||
|
|
||||||
|
export class Authorization extends Schema.Class<Authorization>("ProviderAuthAuthorization")({
|
||||||
|
url: Schema.String,
|
||||||
|
method: Schema.Literals(["auto", "code"]),
|
||||||
|
instructions: Schema.String,
|
||||||
|
}) {
|
||||||
|
static readonly zod = zod(this)
|
||||||
|
}
|
||||||
|
|
||||||
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
|
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
|
||||||
|
|
||||||
@@ -94,7 +86,7 @@ export namespace ProviderAuth {
|
|||||||
type Hook = NonNullable<Hooks["auth"]>
|
type Hook = NonNullable<Hooks["auth"]>
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly methods: () => Effect.Effect<Record<ProviderID, Method[]>>
|
readonly methods: () => Effect.Effect<Methods>
|
||||||
readonly authorize: (input: {
|
readonly authorize: (input: {
|
||||||
providerID: ProviderID
|
providerID: ProviderID
|
||||||
method: number
|
method: number
|
||||||
@@ -131,11 +123,12 @@ export namespace ProviderAuth {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const decode = Schema.decodeUnknownSync(Methods)
|
||||||
const methods = Effect.fn("ProviderAuth.methods")(function* () {
|
const methods = Effect.fn("ProviderAuth.methods")(function* () {
|
||||||
const hooks = (yield* InstanceState.get(state)).hooks
|
const hooks = (yield* InstanceState.get(state)).hooks
|
||||||
return Record.map(hooks, (item) =>
|
return decode(
|
||||||
item.methods.map(
|
Record.map(hooks, (item) =>
|
||||||
(method): Method => ({
|
item.methods.map((method) => ({
|
||||||
type: method.type,
|
type: method.type,
|
||||||
label: method.label,
|
label: method.label,
|
||||||
prompts: method.prompts?.map((prompt) => {
|
prompts: method.prompts?.map((prompt) => {
|
||||||
@@ -156,7 +149,7 @@ export namespace ProviderAuth {
|
|||||||
when: prompt.when,
|
when: prompt.when,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}),
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ProviderAuth } from "@/provider/auth"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
|
||||||
|
const root = "/experimental/httpapi/provider"
|
||||||
|
|
||||||
|
export const ProviderApi = HttpApi.make("provider")
|
||||||
|
.add(
|
||||||
|
HttpApiGroup.make("provider")
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.get("auth", `${root}/auth`, {
|
||||||
|
success: ProviderAuth.Methods,
|
||||||
|
}).annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
identifier: "provider.auth",
|
||||||
|
summary: "Get provider auth methods",
|
||||||
|
description: "Retrieve available authentication methods for all AI providers.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
title: "provider",
|
||||||
|
description: "Experimental HttpApi provider routes.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
title: "opencode experimental HttpApi",
|
||||||
|
version: "0.0.1",
|
||||||
|
description: "Experimental HttpApi surface for selected instance routes.",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ProviderLive = Layer.unwrap(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const svc = yield* ProviderAuth.Service
|
||||||
|
|
||||||
|
const auth = Effect.fn("ProviderHttpApi.auth")(function* () {
|
||||||
|
return yield* svc.methods()
|
||||||
|
})
|
||||||
|
|
||||||
|
return HttpApiBuilder.group(ProviderApi, "provider", (handlers) => handlers.handle("auth", auth))
|
||||||
|
}),
|
||||||
|
).pipe(Layer.provide(ProviderAuth.defaultLayer))
|
||||||
@@ -10,8 +10,10 @@ import { InstanceBootstrap } from "@/project/bootstrap"
|
|||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
|
import { ProviderAuth } from "@/provider/auth"
|
||||||
import { Question } from "@/question"
|
import { Question } from "@/question"
|
||||||
import { PermissionApi, PermissionLive } from "./permission"
|
import { PermissionApi, PermissionLive } from "./permission"
|
||||||
|
import { ProviderApi, ProviderLive } from "./provider"
|
||||||
import { QuestionApi, QuestionLive } from "./question"
|
import { QuestionApi, QuestionLive } from "./question"
|
||||||
|
|
||||||
const Query = Schema.Struct({
|
const Query = Schema.Struct({
|
||||||
@@ -108,6 +110,7 @@ export namespace ExperimentalHttpApiServer {
|
|||||||
|
|
||||||
const QuestionSecured = QuestionApi.middleware(Authorization)
|
const QuestionSecured = QuestionApi.middleware(Authorization)
|
||||||
const PermissionSecured = PermissionApi.middleware(Authorization)
|
const PermissionSecured = PermissionApi.middleware(Authorization)
|
||||||
|
const ProviderSecured = ProviderApi.middleware(Authorization)
|
||||||
|
|
||||||
export const routes = Layer.mergeAll(
|
export const routes = Layer.mergeAll(
|
||||||
HttpApiBuilder.layer(QuestionSecured, { openapiPath: "/experimental/httpapi/question/doc" }).pipe(
|
HttpApiBuilder.layer(QuestionSecured, { openapiPath: "/experimental/httpapi/question/doc" }).pipe(
|
||||||
@@ -116,6 +119,9 @@ export namespace ExperimentalHttpApiServer {
|
|||||||
HttpApiBuilder.layer(PermissionSecured, { openapiPath: "/experimental/httpapi/permission/doc" }).pipe(
|
HttpApiBuilder.layer(PermissionSecured, { openapiPath: "/experimental/httpapi/permission/doc" }).pipe(
|
||||||
Layer.provide(PermissionLive),
|
Layer.provide(PermissionLive),
|
||||||
),
|
),
|
||||||
|
HttpApiBuilder.layer(ProviderSecured, { openapiPath: "/experimental/httpapi/provider/doc" }).pipe(
|
||||||
|
Layer.provide(ProviderLive),
|
||||||
|
),
|
||||||
).pipe(Layer.provide(auth), Layer.provide(normalize), Layer.provide(instance))
|
).pipe(Layer.provide(auth), Layer.provide(normalize), Layer.provide(instance))
|
||||||
|
|
||||||
export const layer = (opts: { hostname: string; port: number }) =>
|
export const layer = (opts: { hostname: string; port: number }) =>
|
||||||
@@ -127,5 +133,6 @@ export namespace ExperimentalHttpApiServer {
|
|||||||
Layer.provideMerge(NodeHttpServer.layerTest),
|
Layer.provideMerge(NodeHttpServer.layerTest),
|
||||||
Layer.provideMerge(Question.defaultLayer),
|
Layer.provideMerge(Question.defaultLayer),
|
||||||
Layer.provideMerge(Permission.defaultLayer),
|
Layer.provideMerge(Permission.defaultLayer),
|
||||||
|
Layer.provideMerge(ProviderAuth.defaultLayer),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const ProviderRoutes = lazy(() =>
|
|||||||
description: "Provider auth methods",
|
description: "Provider auth methods",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))),
|
schema: resolver(ProviderAuth.Methods.zod),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -103,7 +103,7 @@ export const ProviderRoutes = lazy(() =>
|
|||||||
description: "Authorization URL and method",
|
description: "Authorization URL and method",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(ProviderAuth.Authorization.optional()),
|
schema: resolver(ProviderAuth.Authorization.zod.optional()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
|
||||||
"name": "@opencode-ai/server",
|
|
||||||
"version": "1.4.6",
|
|
||||||
"type": "module",
|
|
||||||
"license": "MIT",
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts",
|
|
||||||
"./openapi": "./src/openapi.ts",
|
|
||||||
"./definition": "./src/definition/index.ts",
|
|
||||||
"./definition/api": "./src/definition/api.ts",
|
|
||||||
"./definition/question": "./src/definition/question.ts",
|
|
||||||
"./api": "./src/api/index.ts",
|
|
||||||
"./api/question": "./src/api/question.ts"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"typecheck": "tsgo --noEmit",
|
|
||||||
"build": "tsc"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
"typescript": "catalog:"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"effect": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { makeQuestionHandler } from "./question.js"
|
|
||||||
export type { QuestionOps } from "./question.js"
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { Effect, Schema } from "effect"
|
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
|
||||||
import { QuestionReply, QuestionRequest, questionApi } from "../definition/question.js"
|
|
||||||
|
|
||||||
export interface QuestionOps<R = never> {
|
|
||||||
readonly list: () => Effect.Effect<ReadonlyArray<unknown>, never, R>
|
|
||||||
readonly reply: (input: {
|
|
||||||
requestID: string
|
|
||||||
answers: Schema.Schema.Type<typeof QuestionReply>["answers"]
|
|
||||||
}) => Effect.Effect<void, never, R>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeQuestionHandler = <R>(ops: QuestionOps<R>) =>
|
|
||||||
HttpApiBuilder.group(
|
|
||||||
questionApi,
|
|
||||||
"question",
|
|
||||||
Effect.fn("QuestionHttpApi.handlers")(function* (handlers) {
|
|
||||||
const decode = Schema.decodeUnknownSync(Schema.Array(QuestionRequest))
|
|
||||||
|
|
||||||
const list = Effect.fn("QuestionHttpApi.list")(function* () {
|
|
||||||
return decode(yield* ops.list())
|
|
||||||
})
|
|
||||||
|
|
||||||
const reply = Effect.fn("QuestionHttpApi.reply")(function* (ctx: {
|
|
||||||
params: { requestID: string }
|
|
||||||
payload: Schema.Schema.Type<typeof QuestionReply>
|
|
||||||
}) {
|
|
||||||
yield* ops.reply({
|
|
||||||
requestID: ctx.params.requestID,
|
|
||||||
answers: ctx.payload.answers,
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return handlers.handle("list", list).handle("reply", reply)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { HttpApi, OpenApi } from "effect/unstable/httpapi"
|
|
||||||
import { questionApi } from "./question.js"
|
|
||||||
|
|
||||||
export const api = HttpApi.make("opencode")
|
|
||||||
.addHttpApi(questionApi)
|
|
||||||
.annotateMerge(
|
|
||||||
OpenApi.annotations({
|
|
||||||
title: "opencode experimental HttpApi",
|
|
||||||
version: "0.0.1",
|
|
||||||
description: "Experimental HttpApi surface for selected instance routes.",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { api } from "./api.js"
|
|
||||||
export { questionApi, QuestionReply, QuestionRequest } from "./question.js"
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { Schema } from "effect"
|
|
||||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
|
||||||
|
|
||||||
const root = "/experimental/httpapi/question"
|
|
||||||
|
|
||||||
// Temporary transport-local schemas until canonical question schemas move into packages/core.
|
|
||||||
export const QuestionID = Schema.String.annotate({ identifier: "QuestionID" })
|
|
||||||
export const SessionID = Schema.String.annotate({ identifier: "SessionID" })
|
|
||||||
export const MessageID = Schema.String.annotate({ identifier: "MessageID" })
|
|
||||||
|
|
||||||
export class QuestionOption extends Schema.Class<QuestionOption>("QuestionOption")({
|
|
||||||
label: Schema.String.annotate({
|
|
||||||
description: "Display text (1-5 words, concise)",
|
|
||||||
}),
|
|
||||||
description: Schema.String.annotate({
|
|
||||||
description: "Explanation of choice",
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
const base = {
|
|
||||||
question: Schema.String.annotate({
|
|
||||||
description: "Complete question",
|
|
||||||
}),
|
|
||||||
header: Schema.String.annotate({
|
|
||||||
description: "Very short label (max 30 chars)",
|
|
||||||
}),
|
|
||||||
options: Schema.Array(QuestionOption).annotate({
|
|
||||||
description: "Available choices",
|
|
||||||
}),
|
|
||||||
multiple: Schema.optional(Schema.Boolean).annotate({
|
|
||||||
description: "Allow selecting multiple choices",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QuestionInfo extends Schema.Class<QuestionInfo>("QuestionInfo")({
|
|
||||||
...base,
|
|
||||||
custom: Schema.optional(Schema.Boolean).annotate({
|
|
||||||
description: "Allow typing a custom answer (default: true)",
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class QuestionTool extends Schema.Class<QuestionTool>("QuestionTool")({
|
|
||||||
messageID: MessageID,
|
|
||||||
callID: Schema.String,
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class QuestionRequest extends Schema.Class<QuestionRequest>("QuestionRequest")({
|
|
||||||
id: QuestionID,
|
|
||||||
sessionID: SessionID,
|
|
||||||
questions: Schema.Array(QuestionInfo).annotate({
|
|
||||||
description: "Questions to ask",
|
|
||||||
}),
|
|
||||||
tool: Schema.optional(QuestionTool),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export const QuestionAnswer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" })
|
|
||||||
|
|
||||||
export class QuestionReply extends Schema.Class<QuestionReply>("QuestionReply")({
|
|
||||||
answers: Schema.Array(QuestionAnswer).annotate({
|
|
||||||
description: "User answers in order of questions (each answer is an array of selected labels)",
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export const questionApi = HttpApi.make("question").add(
|
|
||||||
HttpApiGroup.make("question")
|
|
||||||
.add(
|
|
||||||
HttpApiEndpoint.get("list", root, {
|
|
||||||
success: Schema.Array(QuestionRequest),
|
|
||||||
}).annotateMerge(
|
|
||||||
OpenApi.annotations({
|
|
||||||
identifier: "question.list",
|
|
||||||
summary: "List pending questions",
|
|
||||||
description: "Get all pending question requests across all sessions.",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
|
||||||
params: { requestID: QuestionID },
|
|
||||||
payload: QuestionReply,
|
|
||||||
success: Schema.Boolean,
|
|
||||||
}).annotateMerge(
|
|
||||||
OpenApi.annotations({
|
|
||||||
identifier: "question.reply",
|
|
||||||
summary: "Reply to question request",
|
|
||||||
description: "Provide answers to a question request from the AI assistant.",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.annotateMerge(
|
|
||||||
OpenApi.annotations({
|
|
||||||
title: "question",
|
|
||||||
description: "Experimental HttpApi question routes.",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export { openapi } from "./openapi.js"
|
|
||||||
export { makeQuestionHandler } from "./api/question.js"
|
|
||||||
export { api } from "./definition/api.js"
|
|
||||||
export { questionApi, QuestionReply, QuestionRequest } from "./definition/question.js"
|
|
||||||
export type { OpenApiSpec, ServerApi } from "./types.js"
|
|
||||||
export type { QuestionOps } from "./api/question.js"
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { OpenApi } from "effect/unstable/httpapi"
|
|
||||||
import { api } from "./definition/api.js"
|
|
||||||
import type { OpenApiSpec } from "./types.js"
|
|
||||||
|
|
||||||
export const openapi = (): OpenApiSpec => OpenApi.fromApi(api)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import type { HttpApi, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
|
||||||
|
|
||||||
export type ServerApi = HttpApi.HttpApi<string, HttpApiGroup.Any>
|
|
||||||
|
|
||||||
export type OpenApiSpec = OpenApi.OpenAPISpec
|
|
||||||
Vendored
-10
@@ -1,10 +0,0 @@
|
|||||||
/* This file is auto-generated by SST. Do not edit. */
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/* deno-fmt-ignore-file */
|
|
||||||
/* biome-ignore-all lint: auto-generated */
|
|
||||||
|
|
||||||
/// <reference path="../../sst-env.d.ts" />
|
|
||||||
|
|
||||||
import "sst"
|
|
||||||
export {}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"rootDir": "src",
|
|
||||||
"outDir": "dist",
|
|
||||||
"module": "nodenext",
|
|
||||||
"declaration": true,
|
|
||||||
"moduleResolution": "nodenext",
|
|
||||||
"lib": ["es2022", "dom", "dom.iterable"],
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user