Compare commits

..

1 Commits

Author SHA1 Message Date
Filip 5ceeeb3e2c fix(core): route Vertex multi-regions through REP 2026-08-14 19:28:36 +00:00
7 changed files with 75 additions and 122 deletions
@@ -26,6 +26,7 @@ function resolveLocation(options: Record<string, any>) {
function vertexEndpoint(location: string) {
if (location === "global") return "aiplatform.googleapis.com"
if (location === "eu" || location === "us") return `aiplatform.${location}.rep.googleapis.com`
return `${location}-aiplatform.googleapis.com`
}
@@ -111,13 +112,18 @@ export const GoogleVertexPlugin = define({
if (evt.package !== "@ai-sdk/google-vertex") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex"))
const project = resolveProject(evt.options)
const location = resolveLocation(evt.options)
const location = String(resolveLocation(evt.options))
const options = { ...evt.options }
delete options.fetch
evt.sdk = mod.createVertex({
...options,
project,
location,
...((location === "eu" || location === "us") && project && !options.apiKey && !options.baseURL
? {
baseURL: `https://${vertexEndpoint(location)}/v1beta1/projects/${project}/locations/${location}/publishers/google`,
}
: {}),
})
}),
)
@@ -230,7 +230,7 @@ describe("GoogleVertexPlugin", () => {
),
)
it.effect("keeps OpenAI-compatible Vertex endpoint templates regional for eu", () =>
it.effect("uses the REP endpoint for OpenAI-compatible Vertex multi-regions", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
@@ -248,11 +248,65 @@ describe("GoogleVertexPlugin", () => {
const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex")))
expect(provider).toMatchObject({
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" },
settings: {
baseURL: "https://aiplatform.eu.rep.googleapis.com/v1/projects/config-project/locations/eu",
},
})
}),
)
it.effect("uses the REP endpoint for the native Vertex SDK in multi-regions", () =>
withEnv(
{
GOOGLE_CLOUD_PROJECT: "env-project",
GOOGLE_VERTEX_LOCATION: "us",
},
() =>
Effect.gen(function* () {
vertexOptions.length = 0
const aisdk = yield* AISDK.Service
yield* addPlugin()
yield* aisdk.runSDK({
model: Model.Info.make({
...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("gemini")),
modelID: Model.ID.make("gemini"),
package: "aisdk:@ai-sdk/google-vertex",
}),
package: "@ai-sdk/google-vertex",
options: { name: "google-vertex" },
})
expect(vertexOptions).toHaveLength(1)
expect(vertexOptions[0].baseURL).toBe(
"https://aiplatform.us.rep.googleapis.com/v1beta1/projects/env-project/locations/us/publishers/google",
)
}),
),
)
it.effect("preserves custom native Vertex base URLs in multi-regions", () =>
Effect.gen(function* () {
vertexOptions.length = 0
const aisdk = yield* AISDK.Service
yield* addPlugin()
yield* aisdk.runSDK({
model: Model.Info.make({
...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("gemini")),
modelID: Model.ID.make("gemini"),
package: "aisdk:@ai-sdk/google-vertex",
}),
package: "@ai-sdk/google-vertex",
options: {
name: "google-vertex",
project: "project",
location: "eu",
baseURL: "https://vertex.example/v1",
},
})
expect(vertexOptions).toHaveLength(1)
expect(vertexOptions[0].baseURL).toBe("https://vertex.example/v1")
}),
)
it.effect("defaults location to us-central1 when only project is configured", () =>
withEnv(
{
+1 -1
View File
@@ -171,7 +171,7 @@ export interface SlotMap {
readonly "prompt.footer.file": PromptFooterInput
readonly "session.composer.top": { readonly sessionID: string }
readonly "sidebar.content": { readonly sessionID: string }
readonly "sidebar.footer": { readonly sessionID: string }
readonly "sidebar.footer": Readonly<Record<string, never>>
}
export type SlotPath = keyof SlotMap
+7 -22
View File
@@ -70,7 +70,6 @@ import {
import { DialogImagePreview } from "../dialog-image-preview"
import { useDirectoryRecents } from "../../prompt/directory-recents"
import { directoryRecentValue } from "../../prompt/directory-completion"
import { useWorkingDirectoryActions } from "../../ui/working-directory-actions"
export type PromptProps = {
sessionID?: string
@@ -1540,25 +1539,21 @@ export function Prompt(props: PromptProps) {
const width = dimensions().width < 44 ? dimensions().width - 5 : Math.min(75, dimensions().width - 4) - 5
return Locale.takeWidth(value, Math.max(1, width)).trimEnd()
})
const footerLocation = createMemo(() => {
const locationLabel = createMemo(() => {
if (!props.sessionID) {
// No session yet: show where the next session will be created.
return currentLocation.ref ?? data.location.default()
const location = currentLocation.ref ?? data.location.default()
const directory = abbreviateHome(location.directory, paths.home)
const branch = data.location.vcs.info(location)?.branch.current
return branch ? `${directory}:${branch}` : directory
}
if (status() !== "idle") return
return data.session.get(props.sessionID)?.location
})
const locationLabel = createMemo(() => {
const location = footerLocation()
const location = data.session.get(props.sessionID)?.location
if (!location) return
const directory = abbreviateHome(location.directory, paths.home)
const branch = data.location.vcs.info(location)?.branch.current
return branch ? `${directory}:${branch}` : directory
})
const locationActions = useWorkingDirectoryActions({
directory: () => footerLocation()?.directory,
onMove: () => void move.open(),
})
const spinnerDef = createMemo(() => {
const agent = status() === "running" ? local.agent.current() : local.agent.current()
@@ -1879,17 +1874,7 @@ export function Prompt(props: PromptProps) {
<Match when={true}>
<Show when={!props.hint && locationLabel()} fallback={props.hint ?? <text />}>
{(location) => (
<text
id="prompt.footer.location"
fg={locationActions.hovered() ? theme.text.default : theme.text.subdued}
wrapMode="none"
truncate
flexGrow={1}
flexShrink={1}
onMouseOver={locationActions.onMouseOver}
onMouseOut={locationActions.onMouseOut}
onMouseUp={locationActions.onMouseUp}
>
<text fg={theme.text.subdued} wrapMode="none" truncate flexGrow={1} flexShrink={1}>
{location()}
</text>
)}
@@ -1,18 +1,8 @@
import { Plugin } from "@opencode-ai/plugin/tui"
import { createMemo, Show } from "solid-js"
import { FilePath } from "../../ui/file-path"
import { useWorkingDirectoryActions } from "../../ui/working-directory-actions"
import { usePromptMove } from "../../component/prompt/move"
function View(props: { context: Plugin.Context; sessionID: string }) {
const move = usePromptMove({
projectID: () => props.context.data.session.get(props.sessionID)?.projectID,
sessionID: () => props.sessionID,
})
const actions = useWorkingDirectoryActions({
directory: () => props.context.location?.directory,
onMove: () => void move.open(),
})
function View(props: { context: Plugin.Context }) {
const directory = createMemo(() => {
if (!props.context.location) return undefined
const value = props.context.ui.format.path(props.context.location.directory)
@@ -21,20 +11,7 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
})
return (
<Show when={directory()}>
{(value) => (
<box
id="sidebar.footer.location"
onMouseOver={actions.onMouseOver}
onMouseOut={actions.onMouseOut}
onMouseUp={actions.onMouseUp}
>
<FilePath
value={value()}
maxWidth={38}
fg={actions.hovered() ? props.context.theme.text.default : props.context.theme.text.subdued}
/>
</box>
)}
{(value) => <FilePath value={value()} maxWidth={38} fg={props.context.theme.text.subdued} />}
</Show>
)
}
@@ -44,9 +21,6 @@ export default Plugin.define({
setup(context) {
// Append keeps the path open to additive plugin claims; an external
// replace still takes the boundary over.
context.ui.slot({
append: "sidebar.footer",
render: (props) => <View context={context} sessionID={props.sessionID} />,
})
context.ui.slot({ append: "sidebar.footer", render: () => <View context={context} /> })
},
})
+1 -1
View File
@@ -57,7 +57,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
</scrollbox>
<box flexShrink={0} gap={1} paddingTop={1}>
<Slot path="sidebar.footer" input={{ sessionID: props.sessionID }} />
<Slot path="sidebar.footer" />
</box>
</box>
</Show>
@@ -1,66 +0,0 @@
import { createSignal } from "solid-js"
import open from "open"
import { useRenderer } from "@opentui/solid"
import { useClipboard } from "../context/clipboard"
import { useDialog } from "./dialog"
import { DialogSelect } from "./dialog-select"
import { useToast } from "./toast"
export function useWorkingDirectoryActions(input: { directory: () => string | undefined; onMove?: () => void }) {
const clipboard = useClipboard()
const dialog = useDialog()
const renderer = useRenderer()
const toast = useToast()
const [hovered, setHovered] = createSignal(false)
function openMenu() {
if (renderer.getSelection()?.getSelectedText()) return
const directory = input.directory()
if (!directory) return
dialog.replace(() => (
<DialogSelect
title="Working directory"
renderFilter={false}
options={[
{
title: "Copy path",
value: "location.copy",
description: directory,
onSelect: (dialog) => {
void clipboard.write(directory).then(() => {
dialog.clear()
toast.show({ message: "Path copied to clipboard", variant: "info" })
}, toast.error)
},
},
{
title: "Open folder",
value: "location.open",
description: "in system file manager",
onSelect: (dialog) => {
dialog.clear()
void open(directory).catch(toast.error)
},
},
...(input.onMove
? [
{
title: "Move session",
value: "session.move",
description: "to another working directory",
onSelect: () => void input.onMove?.(),
},
]
: []),
]}
/>
))
}
return {
hovered,
onMouseOver: () => setHovered(true),
onMouseOut: () => setHovered(false),
onMouseUp: openMenu,
}
}