Skip to content
Draft
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
54 changes: 21 additions & 33 deletions packages/appkit/src/agents/databricks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type StreamBody,
stream as servingStream,
streamAiGateway,
type WorkspaceRequestClientLike,
} from "../connectors/serving/client";
import { APPKIT_USER_AGENT, getClientOptions } from "../context/client-options";
import { createWorkspaceClient } from "../workspace-client";
Expand Down Expand Up @@ -149,7 +150,7 @@ interface RawFetchAdapterOptions {
* Preferred options: caller provides the transport function directly.
* The `fromServingEndpoint` / `fromModelServing` factories use this to route
* through `connectors/serving/stream`, which centralises URL encoding, auth
* via the SDK's `apiClient.request`, and any future retries/telemetry.
* via the workspace client's `request()`, and any future retries/telemetry.
*/
interface StreamBodyAdapterOptions {
streamBody: StreamBody;
Expand All @@ -174,17 +175,19 @@ function isStreamBodyOptions(
}

/**
* Duck-typed subset of the Databricks SDK `WorkspaceClient`. Callers of
* `fromServingEndpoint` and `fromModelServing` pass a real `WorkspaceClient`,
* but we only need the `apiClient.request` surface — so we declare the minimal
* interface rather than importing the SDK type directly. This keeps the adapter
* free of a hard compile-time dependency on `@databricks/sdk-experimental`.
* Duck-typed subset of the AppKit workspace client. Callers of
* `fromServingEndpoint` and `fromModelServing` pass `createWorkspaceClient()`,
* but we only need its raw `request()` seam, so we declare the minimal shape
* rather than importing the client type. A legacy SDK client exposing only
* `apiClient.request` is still accepted (deprecated).
*/
export interface WorkspaceClientLike {
apiClient: {
request(options: Record<string, unknown>): Promise<unknown>;
};
}
type WorkspaceClientLike =
| WorkspaceRequestClientLike
| {
apiClient: {
request(options: Record<string, unknown>): Promise<unknown>;
};
};

/**
* A fixed client, or a provider resolved on every model call. A provider lets
Expand Down Expand Up @@ -403,7 +406,7 @@ export class DatabricksAdapter implements AgentAdapter {
* Creates a DatabricksAdapter for a Databricks Model Serving endpoint.
*
* Routes through the shared `connectors/serving/stream` helper, which
* delegates to the SDK's `apiClient.request({ raw: true })`. That gives the
* delegates to the workspace client's raw `request()`. That gives the
* adapter centralised URL encoding + authentication with the rest of the
* serving surface — no bespoke `fetch()` + `authenticate()` plumbing.
*/
Expand All @@ -423,15 +426,7 @@ export class DatabricksAdapter implements AgentAdapter {
const resolveClient = clientResolver(workspaceClient);
const adapter = new DatabricksAdapter({
streamBody: (body, signal) =>
// Cast through the structural shape: the connector types
// `workspaceClient` as the SDK's concrete `WorkspaceClient`, but we
// only need `apiClient.request`.
servingStream(
resolveClient() as unknown as Parameters<typeof servingStream>[0],
endpointName,
body,
signal,
),
servingStream(resolveClient(), endpointName, body, signal),
maxSteps,
maxTokens,
generationParams,
Expand Down Expand Up @@ -481,7 +476,7 @@ export class DatabricksAdapter implements AgentAdapter {
if (!workspaceClient) {
workspaceClient = createWorkspaceClient({
clientOptions: getClientOptions(),
}) as unknown as WorkspaceClientLike;
});
}

return DatabricksAdapter.fromServingEndpoint({
Expand All @@ -503,7 +498,7 @@ export class DatabricksAdapter implements AgentAdapter {
* Unlike {@link fromModelServing}, the target model is named in the request
* body (`model`, e.g. `"system.ai.claude-opus-5-5"`) rather than in the URL:
* the gateway is a single fixed path that routes by the body's `model`. Auth
* and transport reuse the SDK's `apiClient.request`, same as the serving
* and transport reuse the workspace client's `request()`, same as the serving
* path, so no bespoke `fetch()` + token handling. The request/response wire
* format and tool-calling loop are identical to the serving path.
*
Expand Down Expand Up @@ -546,21 +541,14 @@ export class DatabricksAdapter implements AgentAdapter {

const resolveClient = clientResolver(
workspaceClient ??
(createWorkspaceClient({
createWorkspaceClient({
clientOptions: getClientOptions(),
}) as unknown as WorkspaceClientLike),
}),
);

const adapter = new DatabricksAdapter({
streamBody: (body, signal) =>
// Same structural cast as `fromServingEndpoint`: the connector types
// the client as the SDK's `WorkspaceClient`, but we only need
// `apiClient.request`.
streamAiGateway(
resolveClient() as unknown as Parameters<typeof streamAiGateway>[0],
body,
signal,
),
streamAiGateway(resolveClient(), body, signal),
model,
maxSteps,
maxTokens,
Expand Down
36 changes: 15 additions & 21 deletions packages/appkit/src/agents/supervisor-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type ApiClientLike,
type StreamBody,
streamPath,
type WorkspaceRequestClientLike,
} from "../connectors/serving/client";
import { createLogger } from "../logging/logger";
import { readSseEvents } from "../stream";
Expand Down Expand Up @@ -90,16 +91,17 @@ function summariseErrorPayload(payload: unknown): string {

/**
* Structural shape of a Databricks SDK client used by {@link fromSupervisorApi}.
* Only what we need: `apiClient.request` for streaming and
* `config.ensureResolved` to materialise the host/credentials.
* Only what we need: `request` for streaming and `getHost` to materialise
* the host/credentials up front. A legacy SDK client (`apiClient.request` +
* `config.ensureResolved`) is still accepted (deprecated).
*
* Exported because {@link SupervisorApiAdapterOptions.workspaceClient} (a
* public type) references it — callers passing their own client can name
* the shape they need to satisfy.
*/
export interface WorkspaceClientLike extends ApiClientLike {
config: { ensureResolved(): Promise<void> };
}
export type WorkspaceClientLike =
| (WorkspaceRequestClientLike & { getHost(): Promise<string> })
| (ApiClientLike & { config: { ensureResolved(): Promise<void> } });

// ---------------------------------------------------------------------------
// Supervisor API tool surface (wire format)
Expand Down Expand Up @@ -346,8 +348,8 @@ interface SupervisorApiAdapterCtorOptions {
* Authentication is handled via the Databricks SDK credential chain — the
* same mechanism used by `DatabricksAdapter.fromModelServing`. The transport
* is injected via {@link SupervisorApiAdapterCtorOptions.streamBody}; the
* {@link fromSupervisorApi} factory wires it through the SDK's
* `apiClient.request({ raw: true })`.
* {@link fromSupervisorApi} factory wires it through the workspace client's
* raw `request()`.
*
* Set `DEBUG=appkit:agents:supervisor-api` to log the outbound request
* shape (model, instructions length, input shape, tool count) and to be
Expand Down Expand Up @@ -877,22 +879,14 @@ function mapEvent(
export async function fromSupervisorApi(
options: SupervisorApiAdapterOptions,
): Promise<AgentAdapter> {
let client = options.workspaceClient;
if (!client) {
// The wrapper's client provides everything `WorkspaceClientLike` needs
// (`apiClient.request` + `config.ensureResolved`) but its
// `apiClient.request` signature is narrower than our structural
// `Record<string, unknown>` shape, so a direct assignment doesn't type.
// The cast bridges the structural gap — same pattern the serving
// connector uses for `ApiClientLike`.
client = createWorkspaceClient() as unknown as WorkspaceClientLike;
}
const resolved: WorkspaceClientLike =
options.workspaceClient ?? createWorkspaceClient();

await client.config.ensureResolved();
// Resolve host + credentials now so misconfiguration fails at construction,
// not on the first stream.
if ("request" in resolved) await resolved.getHost();
else await resolved.config.ensureResolved();

// Capture the resolved client so the closure doesn't depend on the outer
// `let` binding being reassigned later.
const resolved = client;
return new SupervisorApiAdapter({
streamBody: (body, signal) =>
streamPath(resolved, "/ai-gateway/mlflow/v1/responses", body, signal),
Expand Down
Loading